1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license 2 3# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. 4# 5# Permission to use, copy, modify, and distribute this software and its 6# documentation for any purpose with or without fee is hereby granted, 7# provided that the above copyright notice and this permission notice 8# appear in all copies. 9# 10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES 11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR 13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 18"""DNS Zones.""" 19 20import contextlib 21import io 22import os 23import re 24import sys 25 26import dns.exception 27import dns.name 28import dns.node 29import dns.rdataclass 30import dns.rdatatype 31import dns.rdata 32import dns.rdtypes.ANY.SOA 33import dns.rrset 34import dns.tokenizer 35import dns.ttl 36import dns.grange 37 38 39class BadZone(dns.exception.DNSException): 40 41 """The DNS zone is malformed.""" 42 43 44class NoSOA(BadZone): 45 46 """The DNS zone has no SOA RR at its origin.""" 47 48 49class NoNS(BadZone): 50 51 """The DNS zone has no NS RRset at its origin.""" 52 53 54class UnknownOrigin(BadZone): 55 56 """The DNS zone's origin is unknown.""" 57 58 59class Zone: 60 61 """A DNS zone. 62 63 A ``Zone`` is a mapping from names to nodes. The zone object may be 64 treated like a Python dictionary, e.g. ``zone[name]`` will retrieve 65 the node associated with that name. The *name* may be a 66 ``dns.name.Name object``, or it may be a string. In either case, 67 if the name is relative it is treated as relative to the origin of 68 the zone. 69 """ 70 71 node_factory = dns.node.Node 72 73 __slots__ = ['rdclass', 'origin', 'nodes', 'relativize'] 74 75 def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True): 76 """Initialize a zone object. 77 78 *origin* is the origin of the zone. It may be a ``dns.name.Name``, 79 a ``str``, or ``None``. If ``None``, then the zone's origin will 80 be set by the first ``$ORIGIN`` line in a masterfile. 81 82 *rdclass*, an ``int``, the zone's rdata class; the default is class IN. 83 84 *relativize*, a ``bool``, determine's whether domain names are 85 relativized to the zone's origin. The default is ``True``. 86 """ 87 88 if origin is not None: 89 if isinstance(origin, str): 90 origin = dns.name.from_text(origin) 91 elif not isinstance(origin, dns.name.Name): 92 raise ValueError("origin parameter must be convertible to a " 93 "DNS name") 94 if not origin.is_absolute(): 95 raise ValueError("origin parameter must be an absolute name") 96 self.origin = origin 97 self.rdclass = rdclass 98 self.nodes = {} 99 self.relativize = relativize 100 101 def __eq__(self, other): 102 """Two zones are equal if they have the same origin, class, and 103 nodes. 104 105 Returns a ``bool``. 106 """ 107 108 if not isinstance(other, Zone): 109 return False 110 if self.rdclass != other.rdclass or \ 111 self.origin != other.origin or \ 112 self.nodes != other.nodes: 113 return False 114 return True 115 116 def __ne__(self, other): 117 """Are two zones not equal? 118 119 Returns a ``bool``. 120 """ 121 122 return not self.__eq__(other) 123 124 def _validate_name(self, name): 125 if isinstance(name, str): 126 name = dns.name.from_text(name, None) 127 elif not isinstance(name, dns.name.Name): 128 raise KeyError("name parameter must be convertible to a DNS name") 129 if name.is_absolute(): 130 if not name.is_subdomain(self.origin): 131 raise KeyError( 132 "name parameter must be a subdomain of the zone origin") 133 if self.relativize: 134 name = name.relativize(self.origin) 135 return name 136 137 def __getitem__(self, key): 138 key = self._validate_name(key) 139 return self.nodes[key] 140 141 def __setitem__(self, key, value): 142 key = self._validate_name(key) 143 self.nodes[key] = value 144 145 def __delitem__(self, key): 146 key = self._validate_name(key) 147 del self.nodes[key] 148 149 def __iter__(self): 150 return self.nodes.__iter__() 151 152 def keys(self): 153 return self.nodes.keys() # pylint: disable=dict-keys-not-iterating 154 155 def values(self): 156 return self.nodes.values() # pylint: disable=dict-values-not-iterating 157 158 def items(self): 159 return self.nodes.items() # pylint: disable=dict-items-not-iterating 160 161 def get(self, key): 162 key = self._validate_name(key) 163 return self.nodes.get(key) 164 165 def __contains__(self, other): 166 return other in self.nodes 167 168 def find_node(self, name, create=False): 169 """Find a node in the zone, possibly creating it. 170 171 *name*: the name of the node to find. 172 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 173 name must be a subdomain of the zone's origin. If ``zone.relativize`` 174 is ``True``, then the name will be relativized. 175 176 *create*, a ``bool``. If true, the node will be created if it does 177 not exist. 178 179 Raises ``KeyError`` if the name is not known and create was 180 not specified, or if the name was not a subdomain of the origin. 181 182 Returns a ``dns.node.Node``. 183 """ 184 185 name = self._validate_name(name) 186 node = self.nodes.get(name) 187 if node is None: 188 if not create: 189 raise KeyError 190 node = self.node_factory() 191 self.nodes[name] = node 192 return node 193 194 def get_node(self, name, create=False): 195 """Get a node in the zone, possibly creating it. 196 197 This method is like ``find_node()``, except it returns None instead 198 of raising an exception if the node does not exist and creation 199 has not been requested. 200 201 *name*: the name of the node to find. 202 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 203 name must be a subdomain of the zone's origin. If ``zone.relativize`` 204 is ``True``, then the name will be relativized. 205 206 *create*, a ``bool``. If true, the node will be created if it does 207 not exist. 208 209 Raises ``KeyError`` if the name is not known and create was 210 not specified, or if the name was not a subdomain of the origin. 211 212 Returns a ``dns.node.Node`` or ``None``. 213 """ 214 215 try: 216 node = self.find_node(name, create) 217 except KeyError: 218 node = None 219 return node 220 221 def delete_node(self, name): 222 """Delete the specified node if it exists. 223 224 *name*: the name of the node to find. 225 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 226 name must be a subdomain of the zone's origin. If ``zone.relativize`` 227 is ``True``, then the name will be relativized. 228 229 It is not an error if the node does not exist. 230 """ 231 232 name = self._validate_name(name) 233 if name in self.nodes: 234 del self.nodes[name] 235 236 def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, 237 create=False): 238 """Look for an rdataset with the specified name and type in the zone, 239 and return an rdataset encapsulating it. 240 241 The rdataset returned is not a copy; changes to it will change 242 the zone. 243 244 KeyError is raised if the name or type are not found. 245 246 *name*: the name of the node to find. 247 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 248 name must be a subdomain of the zone's origin. If ``zone.relativize`` 249 is ``True``, then the name will be relativized. 250 251 *rdtype*, an ``int`` or ``str``, the rdata type desired. 252 253 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 254 Usually this value is ``dns.rdatatype.NONE``, but if the 255 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 256 then the covers value will be the rdata type the SIG/RRSIG 257 covers. The library treats the SIG and RRSIG types as if they 258 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 259 This makes RRSIGs much easier to work with than if RRSIGs 260 covering different rdata types were aggregated into a single 261 RRSIG rdataset. 262 263 *create*, a ``bool``. If true, the node will be created if it does 264 not exist. 265 266 Raises ``KeyError`` if the name is not known and create was 267 not specified, or if the name was not a subdomain of the origin. 268 269 Returns a ``dns.rdataset.Rdataset``. 270 """ 271 272 name = self._validate_name(name) 273 rdtype = dns.rdatatype.RdataType.make(rdtype) 274 if covers is not None: 275 covers = dns.rdatatype.RdataType.make(covers) 276 node = self.find_node(name, create) 277 return node.find_rdataset(self.rdclass, rdtype, covers, create) 278 279 def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, 280 create=False): 281 """Look for an rdataset with the specified name and type in the zone. 282 283 This method is like ``find_rdataset()``, except it returns None instead 284 of raising an exception if the rdataset does not exist and creation 285 has not been requested. 286 287 The rdataset returned is not a copy; changes to it will change 288 the zone. 289 290 *name*: the name of the node to find. 291 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 292 name must be a subdomain of the zone's origin. If ``zone.relativize`` 293 is ``True``, then the name will be relativized. 294 295 *rdtype*, an ``int`` or ``str``, the rdata type desired. 296 297 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 298 Usually this value is ``dns.rdatatype.NONE``, but if the 299 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 300 then the covers value will be the rdata type the SIG/RRSIG 301 covers. The library treats the SIG and RRSIG types as if they 302 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 303 This makes RRSIGs much easier to work with than if RRSIGs 304 covering different rdata types were aggregated into a single 305 RRSIG rdataset. 306 307 *create*, a ``bool``. If true, the node will be created if it does 308 not exist. 309 310 Raises ``KeyError`` if the name is not known and create was 311 not specified, or if the name was not a subdomain of the origin. 312 313 Returns a ``dns.rdataset.Rdataset`` or ``None``. 314 """ 315 316 try: 317 rdataset = self.find_rdataset(name, rdtype, covers, create) 318 except KeyError: 319 rdataset = None 320 return rdataset 321 322 def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE): 323 """Delete the rdataset matching *rdtype* and *covers*, if it 324 exists at the node specified by *name*. 325 326 It is not an error if the node does not exist, or if there is no 327 matching rdataset at the node. 328 329 If the node has no rdatasets after the deletion, it will itself 330 be deleted. 331 332 *name*: the name of the node to find. 333 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 334 name must be a subdomain of the zone's origin. If ``zone.relativize`` 335 is ``True``, then the name will be relativized. 336 337 *rdtype*, an ``int`` or ``str``, the rdata type desired. 338 339 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 340 Usually this value is ``dns.rdatatype.NONE``, but if the 341 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 342 then the covers value will be the rdata type the SIG/RRSIG 343 covers. The library treats the SIG and RRSIG types as if they 344 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 345 This makes RRSIGs much easier to work with than if RRSIGs 346 covering different rdata types were aggregated into a single 347 RRSIG rdataset. 348 """ 349 350 name = self._validate_name(name) 351 rdtype = dns.rdatatype.RdataType.make(rdtype) 352 if covers is not None: 353 covers = dns.rdatatype.RdataType.make(covers) 354 node = self.get_node(name) 355 if node is not None: 356 node.delete_rdataset(self.rdclass, rdtype, covers) 357 if len(node) == 0: 358 self.delete_node(name) 359 360 def replace_rdataset(self, name, replacement): 361 """Replace an rdataset at name. 362 363 It is not an error if there is no rdataset matching I{replacement}. 364 365 Ownership of the *replacement* object is transferred to the zone; 366 in other words, this method does not store a copy of *replacement* 367 at the node, it stores *replacement* itself. 368 369 If the node does not exist, it is created. 370 371 *name*: the name of the node to find. 372 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 373 name must be a subdomain of the zone's origin. If ``zone.relativize`` 374 is ``True``, then the name will be relativized. 375 376 *replacement*, a ``dns.rdataset.Rdataset``, the replacement rdataset. 377 """ 378 379 if replacement.rdclass != self.rdclass: 380 raise ValueError('replacement.rdclass != zone.rdclass') 381 node = self.find_node(name, True) 382 node.replace_rdataset(replacement) 383 384 def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE): 385 """Look for an rdataset with the specified name and type in the zone, 386 and return an RRset encapsulating it. 387 388 This method is less efficient than the similar 389 ``find_rdataset()`` because it creates an RRset instead of 390 returning the matching rdataset. It may be more convenient 391 for some uses since it returns an object which binds the owner 392 name to the rdataset. 393 394 This method may not be used to create new nodes or rdatasets; 395 use ``find_rdataset`` instead. 396 397 *name*: the name of the node to find. 398 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 399 name must be a subdomain of the zone's origin. If ``zone.relativize`` 400 is ``True``, then the name will be relativized. 401 402 *rdtype*, an ``int`` or ``str``, the rdata type desired. 403 404 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 405 Usually this value is ``dns.rdatatype.NONE``, but if the 406 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 407 then the covers value will be the rdata type the SIG/RRSIG 408 covers. The library treats the SIG and RRSIG types as if they 409 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 410 This makes RRSIGs much easier to work with than if RRSIGs 411 covering different rdata types were aggregated into a single 412 RRSIG rdataset. 413 414 *create*, a ``bool``. If true, the node will be created if it does 415 not exist. 416 417 Raises ``KeyError`` if the name is not known and create was 418 not specified, or if the name was not a subdomain of the origin. 419 420 Returns a ``dns.rrset.RRset`` or ``None``. 421 """ 422 423 name = self._validate_name(name) 424 rdtype = dns.rdatatype.RdataType.make(rdtype) 425 if covers is not None: 426 covers = dns.rdatatype.RdataType.make(covers) 427 rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers) 428 rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers) 429 rrset.update(rdataset) 430 return rrset 431 432 def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE): 433 """Look for an rdataset with the specified name and type in the zone, 434 and return an RRset encapsulating it. 435 436 This method is less efficient than the similar ``get_rdataset()`` 437 because it creates an RRset instead of returning the matching 438 rdataset. It may be more convenient for some uses since it 439 returns an object which binds the owner name to the rdataset. 440 441 This method may not be used to create new nodes or rdatasets; 442 use ``get_rdataset()`` instead. 443 444 *name*: the name of the node to find. 445 The value may be a ``dns.name.Name`` or a ``str``. If absolute, the 446 name must be a subdomain of the zone's origin. If ``zone.relativize`` 447 is ``True``, then the name will be relativized. 448 449 *rdtype*, an ``int`` or ``str``, the rdata type desired. 450 451 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 452 Usually this value is ``dns.rdatatype.NONE``, but if the 453 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 454 then the covers value will be the rdata type the SIG/RRSIG 455 covers. The library treats the SIG and RRSIG types as if they 456 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 457 This makes RRSIGs much easier to work with than if RRSIGs 458 covering different rdata types were aggregated into a single 459 RRSIG rdataset. 460 461 *create*, a ``bool``. If true, the node will be created if it does 462 not exist. 463 464 Raises ``KeyError`` if the name is not known and create was 465 not specified, or if the name was not a subdomain of the origin. 466 467 Returns a ``dns.rrset.RRset`` or ``None``. 468 """ 469 470 try: 471 rrset = self.find_rrset(name, rdtype, covers) 472 except KeyError: 473 rrset = None 474 return rrset 475 476 def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY, 477 covers=dns.rdatatype.NONE): 478 """Return a generator which yields (name, rdataset) tuples for 479 all rdatasets in the zone which have the specified *rdtype* 480 and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default, 481 then all rdatasets will be matched. 482 483 *rdtype*, an ``int`` or ``str``, the rdata type desired. 484 485 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 486 Usually this value is ``dns.rdatatype.NONE``, but if the 487 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 488 then the covers value will be the rdata type the SIG/RRSIG 489 covers. The library treats the SIG and RRSIG types as if they 490 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 491 This makes RRSIGs much easier to work with than if RRSIGs 492 covering different rdata types were aggregated into a single 493 RRSIG rdataset. 494 """ 495 496 rdtype = dns.rdatatype.RdataType.make(rdtype) 497 if covers is not None: 498 covers = dns.rdatatype.RdataType.make(covers) 499 for (name, node) in self.items(): 500 for rds in node: 501 if rdtype == dns.rdatatype.ANY or \ 502 (rds.rdtype == rdtype and rds.covers == covers): 503 yield (name, rds) 504 505 def iterate_rdatas(self, rdtype=dns.rdatatype.ANY, 506 covers=dns.rdatatype.NONE): 507 """Return a generator which yields (name, ttl, rdata) tuples for 508 all rdatas in the zone which have the specified *rdtype* 509 and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default, 510 then all rdatas will be matched. 511 512 *rdtype*, an ``int`` or ``str``, the rdata type desired. 513 514 *covers*, an ``int`` or ``str`` or ``None``, the covered type. 515 Usually this value is ``dns.rdatatype.NONE``, but if the 516 rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, 517 then the covers value will be the rdata type the SIG/RRSIG 518 covers. The library treats the SIG and RRSIG types as if they 519 were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). 520 This makes RRSIGs much easier to work with than if RRSIGs 521 covering different rdata types were aggregated into a single 522 RRSIG rdataset. 523 """ 524 525 rdtype = dns.rdatatype.RdataType.make(rdtype) 526 if covers is not None: 527 covers = dns.rdatatype.RdataType.make(covers) 528 for (name, node) in self.items(): 529 for rds in node: 530 if rdtype == dns.rdatatype.ANY or \ 531 (rds.rdtype == rdtype and rds.covers == covers): 532 for rdata in rds: 533 yield (name, rds.ttl, rdata) 534 535 def to_file(self, f, sorted=True, relativize=True, nl=None): 536 """Write a zone to a file. 537 538 *f*, a file or `str`. If *f* is a string, it is treated 539 as the name of a file to open. 540 541 *sorted*, a ``bool``. If True, the default, then the file 542 will be written with the names sorted in DNSSEC order from 543 least to greatest. Otherwise the names will be written in 544 whatever order they happen to have in the zone's dictionary. 545 546 *relativize*, a ``bool``. If True, the default, then domain 547 names in the output will be relativized to the zone's origin 548 if possible. 549 550 *nl*, a ``str`` or None. The end of line string. If not 551 ``None``, the output will use the platform's native 552 end-of-line marker (i.e. LF on POSIX, CRLF on Windows). 553 """ 554 555 with contextlib.ExitStack() as stack: 556 if isinstance(f, str): 557 f = stack.enter_context(open(f, 'wb')) 558 559 # must be in this way, f.encoding may contain None, or even 560 # attribute may not be there 561 file_enc = getattr(f, 'encoding', None) 562 if file_enc is None: 563 file_enc = 'utf-8' 564 565 if nl is None: 566 # binary mode, '\n' is not enough 567 nl_b = os.linesep.encode(file_enc) 568 nl = '\n' 569 elif isinstance(nl, str): 570 nl_b = nl.encode(file_enc) 571 else: 572 nl_b = nl 573 nl = nl.decode() 574 575 if sorted: 576 names = list(self.keys()) 577 names.sort() 578 else: 579 names = self.keys() 580 for n in names: 581 l = self[n].to_text(n, origin=self.origin, 582 relativize=relativize) 583 if isinstance(l, str): 584 l_b = l.encode(file_enc) 585 else: 586 l_b = l 587 l = l.decode() 588 589 try: 590 f.write(l_b) 591 f.write(nl_b) 592 except TypeError: # textual mode 593 f.write(l) 594 f.write(nl) 595 596 def to_text(self, sorted=True, relativize=True, nl=None): 597 """Return a zone's text as though it were written to a file. 598 599 *sorted*, a ``bool``. If True, the default, then the file 600 will be written with the names sorted in DNSSEC order from 601 least to greatest. Otherwise the names will be written in 602 whatever order they happen to have in the zone's dictionary. 603 604 *relativize*, a ``bool``. If True, the default, then domain 605 names in the output will be relativized to the zone's origin 606 if possible. 607 608 *nl*, a ``str`` or None. The end of line string. If not 609 ``None``, the output will use the platform's native 610 end-of-line marker (i.e. LF on POSIX, CRLF on Windows). 611 612 Returns a ``str``. 613 """ 614 temp_buffer = io.StringIO() 615 self.to_file(temp_buffer, sorted, relativize, nl) 616 return_value = temp_buffer.getvalue() 617 temp_buffer.close() 618 return return_value 619 620 def check_origin(self): 621 """Do some simple checking of the zone's origin. 622 623 Raises ``dns.zone.NoSOA`` if there is no SOA RRset. 624 625 Raises ``dns.zone.NoNS`` if there is no NS RRset. 626 627 Raises ``KeyError`` if there is no origin node. 628 """ 629 if self.relativize: 630 name = dns.name.empty 631 else: 632 name = self.origin 633 if self.get_rdataset(name, dns.rdatatype.SOA) is None: 634 raise NoSOA 635 if self.get_rdataset(name, dns.rdatatype.NS) is None: 636 raise NoNS 637 638 639class _MasterReader: 640 641 """Read a DNS master file 642 643 @ivar tok: The tokenizer 644 @type tok: dns.tokenizer.Tokenizer object 645 @ivar last_ttl: The last seen explicit TTL for an RR 646 @type last_ttl: int 647 @ivar last_ttl_known: Has last TTL been detected 648 @type last_ttl_known: bool 649 @ivar default_ttl: The default TTL from a $TTL directive or SOA RR 650 @type default_ttl: int 651 @ivar default_ttl_known: Has default TTL been detected 652 @type default_ttl_known: bool 653 @ivar last_name: The last name read 654 @type last_name: dns.name.Name object 655 @ivar current_origin: The current origin 656 @type current_origin: dns.name.Name object 657 @ivar relativize: should names in the zone be relativized? 658 @type relativize: bool 659 @ivar zone: the zone 660 @type zone: dns.zone.Zone object 661 @ivar saved_state: saved reader state (used when processing $INCLUDE) 662 @type saved_state: list of (tokenizer, current_origin, last_name, file, 663 last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples. 664 @ivar current_file: the file object of the $INCLUDed file being parsed 665 (None if no $INCLUDE is active). 666 @ivar allow_include: is $INCLUDE allowed? 667 @type allow_include: bool 668 @ivar check_origin: should sanity checks of the origin node be done? 669 The default is True. 670 @type check_origin: bool 671 """ 672 673 def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone, 674 allow_include=False, check_origin=True): 675 if isinstance(origin, str): 676 origin = dns.name.from_text(origin) 677 self.tok = tok 678 self.current_origin = origin 679 self.relativize = relativize 680 self.last_ttl = 0 681 self.last_ttl_known = False 682 self.default_ttl = 0 683 self.default_ttl_known = False 684 self.last_name = self.current_origin 685 self.zone = zone_factory(origin, rdclass, relativize=relativize) 686 self.saved_state = [] 687 self.current_file = None 688 self.allow_include = allow_include 689 self.check_origin = check_origin 690 691 def _eat_line(self): 692 while 1: 693 token = self.tok.get() 694 if token.is_eol_or_eof(): 695 break 696 697 def _rr_line(self): 698 """Process one line from a DNS master file.""" 699 # Name 700 if self.current_origin is None: 701 raise UnknownOrigin 702 token = self.tok.get(want_leading=True) 703 if not token.is_whitespace(): 704 self.last_name = self.tok.as_name(token, self.current_origin) 705 else: 706 token = self.tok.get() 707 if token.is_eol_or_eof(): 708 # treat leading WS followed by EOL/EOF as if they were EOL/EOF. 709 return 710 self.tok.unget(token) 711 name = self.last_name 712 if not name.is_subdomain(self.zone.origin): 713 self._eat_line() 714 return 715 if self.relativize: 716 name = name.relativize(self.zone.origin) 717 token = self.tok.get() 718 if not token.is_identifier(): 719 raise dns.exception.SyntaxError 720 721 # TTL 722 ttl = None 723 try: 724 ttl = dns.ttl.from_text(token.value) 725 self.last_ttl = ttl 726 self.last_ttl_known = True 727 token = self.tok.get() 728 if not token.is_identifier(): 729 raise dns.exception.SyntaxError 730 except dns.ttl.BadTTL: 731 if self.default_ttl_known: 732 ttl = self.default_ttl 733 elif self.last_ttl_known: 734 ttl = self.last_ttl 735 736 # Class 737 try: 738 rdclass = dns.rdataclass.from_text(token.value) 739 token = self.tok.get() 740 if not token.is_identifier(): 741 raise dns.exception.SyntaxError 742 except dns.exception.SyntaxError: 743 raise 744 except Exception: 745 rdclass = self.zone.rdclass 746 if rdclass != self.zone.rdclass: 747 raise dns.exception.SyntaxError("RR class is not zone's class") 748 # Type 749 try: 750 rdtype = dns.rdatatype.from_text(token.value) 751 except Exception: 752 raise dns.exception.SyntaxError( 753 "unknown rdatatype '%s'" % token.value) 754 n = self.zone.nodes.get(name) 755 if n is None: 756 n = self.zone.node_factory() 757 self.zone.nodes[name] = n 758 try: 759 rd = dns.rdata.from_text(rdclass, rdtype, self.tok, 760 self.current_origin, self.relativize, 761 self.zone.origin) 762 except dns.exception.SyntaxError: 763 # Catch and reraise. 764 raise 765 except Exception: 766 # All exceptions that occur in the processing of rdata 767 # are treated as syntax errors. This is not strictly 768 # correct, but it is correct almost all of the time. 769 # We convert them to syntax errors so that we can emit 770 # helpful filename:line info. 771 (ty, va) = sys.exc_info()[:2] 772 raise dns.exception.SyntaxError( 773 "caught exception {}: {}".format(str(ty), str(va))) 774 775 if not self.default_ttl_known and rdtype == dns.rdatatype.SOA: 776 # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default 777 # TTL from the SOA minttl if no $TTL statement is present before the 778 # SOA is parsed. 779 self.default_ttl = rd.minimum 780 self.default_ttl_known = True 781 if ttl is None: 782 # if we didn't have a TTL on the SOA, set it! 783 ttl = rd.minimum 784 785 # TTL check. We had to wait until now to do this as the SOA RR's 786 # own TTL can be inferred from its minimum. 787 if ttl is None: 788 raise dns.exception.SyntaxError("Missing default TTL value") 789 790 covers = rd.covers() 791 rds = n.find_rdataset(rdclass, rdtype, covers, True) 792 rds.add(rd, ttl) 793 794 def _parse_modify(self, side): 795 # Here we catch everything in '{' '}' in a group so we can replace it 796 # with ''. 797 is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$") 798 is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$") 799 is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$") 800 # Sometimes there are modifiers in the hostname. These come after 801 # the dollar sign. They are in the form: ${offset[,width[,base]]}. 802 # Make names 803 g1 = is_generate1.match(side) 804 if g1: 805 mod, sign, offset, width, base = g1.groups() 806 if sign == '': 807 sign = '+' 808 g2 = is_generate2.match(side) 809 if g2: 810 mod, sign, offset = g2.groups() 811 if sign == '': 812 sign = '+' 813 width = 0 814 base = 'd' 815 g3 = is_generate3.match(side) 816 if g3: 817 mod, sign, offset, width = g3.groups() 818 if sign == '': 819 sign = '+' 820 base = 'd' 821 822 if not (g1 or g2 or g3): 823 mod = '' 824 sign = '+' 825 offset = 0 826 width = 0 827 base = 'd' 828 829 if base != 'd': 830 raise NotImplementedError() 831 832 return mod, sign, offset, width, base 833 834 def _generate_line(self): 835 # range lhs [ttl] [class] type rhs [ comment ] 836 """Process one line containing the GENERATE statement from a DNS 837 master file.""" 838 if self.current_origin is None: 839 raise UnknownOrigin 840 841 token = self.tok.get() 842 # Range (required) 843 try: 844 start, stop, step = dns.grange.from_text(token.value) 845 token = self.tok.get() 846 if not token.is_identifier(): 847 raise dns.exception.SyntaxError 848 except Exception: 849 raise dns.exception.SyntaxError 850 851 # lhs (required) 852 try: 853 lhs = token.value 854 token = self.tok.get() 855 if not token.is_identifier(): 856 raise dns.exception.SyntaxError 857 except Exception: 858 raise dns.exception.SyntaxError 859 860 # TTL 861 try: 862 ttl = dns.ttl.from_text(token.value) 863 self.last_ttl = ttl 864 self.last_ttl_known = True 865 token = self.tok.get() 866 if not token.is_identifier(): 867 raise dns.exception.SyntaxError 868 except dns.ttl.BadTTL: 869 if not (self.last_ttl_known or self.default_ttl_known): 870 raise dns.exception.SyntaxError("Missing default TTL value") 871 if self.default_ttl_known: 872 ttl = self.default_ttl 873 elif self.last_ttl_known: 874 ttl = self.last_ttl 875 # Class 876 try: 877 rdclass = dns.rdataclass.from_text(token.value) 878 token = self.tok.get() 879 if not token.is_identifier(): 880 raise dns.exception.SyntaxError 881 except dns.exception.SyntaxError: 882 raise dns.exception.SyntaxError 883 except Exception: 884 rdclass = self.zone.rdclass 885 if rdclass != self.zone.rdclass: 886 raise dns.exception.SyntaxError("RR class is not zone's class") 887 # Type 888 try: 889 rdtype = dns.rdatatype.from_text(token.value) 890 token = self.tok.get() 891 if not token.is_identifier(): 892 raise dns.exception.SyntaxError 893 except Exception: 894 raise dns.exception.SyntaxError("unknown rdatatype '%s'" % 895 token.value) 896 897 # rhs (required) 898 rhs = token.value 899 900 lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs) 901 rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs) 902 for i in range(start, stop + 1, step): 903 # +1 because bind is inclusive and python is exclusive 904 905 if lsign == '+': 906 lindex = i + int(loffset) 907 elif lsign == '-': 908 lindex = i - int(loffset) 909 910 if rsign == '-': 911 rindex = i - int(roffset) 912 elif rsign == '+': 913 rindex = i + int(roffset) 914 915 lzfindex = str(lindex).zfill(int(lwidth)) 916 rzfindex = str(rindex).zfill(int(rwidth)) 917 918 name = lhs.replace('$%s' % (lmod), lzfindex) 919 rdata = rhs.replace('$%s' % (rmod), rzfindex) 920 921 self.last_name = dns.name.from_text(name, self.current_origin, 922 self.tok.idna_codec) 923 name = self.last_name 924 if not name.is_subdomain(self.zone.origin): 925 self._eat_line() 926 return 927 if self.relativize: 928 name = name.relativize(self.zone.origin) 929 930 n = self.zone.nodes.get(name) 931 if n is None: 932 n = self.zone.node_factory() 933 self.zone.nodes[name] = n 934 try: 935 rd = dns.rdata.from_text(rdclass, rdtype, rdata, 936 self.current_origin, self.relativize, 937 self.zone.origin) 938 except dns.exception.SyntaxError: 939 # Catch and reraise. 940 raise 941 except Exception: 942 # All exceptions that occur in the processing of rdata 943 # are treated as syntax errors. This is not strictly 944 # correct, but it is correct almost all of the time. 945 # We convert them to syntax errors so that we can emit 946 # helpful filename:line info. 947 (ty, va) = sys.exc_info()[:2] 948 raise dns.exception.SyntaxError("caught exception %s: %s" % 949 (str(ty), str(va))) 950 951 covers = rd.covers() 952 rds = n.find_rdataset(rdclass, rdtype, covers, True) 953 rds.add(rd, ttl) 954 955 def read(self): 956 """Read a DNS master file and build a zone object. 957 958 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin 959 @raises dns.zone.NoNS: No NS RRset was found at the zone origin 960 """ 961 962 try: 963 while 1: 964 token = self.tok.get(True, True) 965 if token.is_eof(): 966 if self.current_file is not None: 967 self.current_file.close() 968 if len(self.saved_state) > 0: 969 (self.tok, 970 self.current_origin, 971 self.last_name, 972 self.current_file, 973 self.last_ttl, 974 self.last_ttl_known, 975 self.default_ttl, 976 self.default_ttl_known) = self.saved_state.pop(-1) 977 continue 978 break 979 elif token.is_eol(): 980 continue 981 elif token.is_comment(): 982 self.tok.get_eol() 983 continue 984 elif token.value[0] == '$': 985 c = token.value.upper() 986 if c == '$TTL': 987 token = self.tok.get() 988 if not token.is_identifier(): 989 raise dns.exception.SyntaxError("bad $TTL") 990 self.default_ttl = dns.ttl.from_text(token.value) 991 self.default_ttl_known = True 992 self.tok.get_eol() 993 elif c == '$ORIGIN': 994 self.current_origin = self.tok.get_name() 995 self.tok.get_eol() 996 if self.zone.origin is None: 997 self.zone.origin = self.current_origin 998 elif c == '$INCLUDE' and self.allow_include: 999 token = self.tok.get() 1000 filename = token.value 1001 token = self.tok.get() 1002 if token.is_identifier(): 1003 new_origin =\ 1004 dns.name.from_text(token.value, 1005 self.current_origin, 1006 self.tok.idna_codec) 1007 self.tok.get_eol() 1008 elif not token.is_eol_or_eof(): 1009 raise dns.exception.SyntaxError( 1010 "bad origin in $INCLUDE") 1011 else: 1012 new_origin = self.current_origin 1013 self.saved_state.append((self.tok, 1014 self.current_origin, 1015 self.last_name, 1016 self.current_file, 1017 self.last_ttl, 1018 self.last_ttl_known, 1019 self.default_ttl, 1020 self.default_ttl_known)) 1021 self.current_file = open(filename, 'r') 1022 self.tok = dns.tokenizer.Tokenizer(self.current_file, 1023 filename) 1024 self.current_origin = new_origin 1025 elif c == '$GENERATE': 1026 self._generate_line() 1027 else: 1028 raise dns.exception.SyntaxError( 1029 "Unknown master file directive '" + c + "'") 1030 continue 1031 self.tok.unget(token) 1032 self._rr_line() 1033 except dns.exception.SyntaxError as detail: 1034 (filename, line_number) = self.tok.where() 1035 if detail is None: 1036 detail = "syntax error" 1037 ex = dns.exception.SyntaxError( 1038 "%s:%d: %s" % (filename, line_number, detail)) 1039 tb = sys.exc_info()[2] 1040 raise ex.with_traceback(tb) from None 1041 1042 # Now that we're done reading, do some basic checking of the zone. 1043 if self.check_origin: 1044 self.zone.check_origin() 1045 1046 1047def from_text(text, origin=None, rdclass=dns.rdataclass.IN, 1048 relativize=True, zone_factory=Zone, filename=None, 1049 allow_include=False, check_origin=True, idna_codec=None): 1050 """Build a zone object from a master file format string. 1051 1052 *text*, a ``str``, the master file format input. 1053 1054 *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin 1055 of the zone; if not specified, the first ``$ORIGIN`` statement in the 1056 masterfile will determine the origin of the zone. 1057 1058 *rdclass*, an ``int``, the zone's rdata class; the default is class IN. 1059 1060 *relativize*, a ``bool``, determine's whether domain names are 1061 relativized to the zone's origin. The default is ``True``. 1062 1063 *zone_factory*, the zone factory to use or ``None``. If ``None``, then 1064 ``dns.zone.Zone`` will be used. The value may be any class or callable 1065 that returns a subclass of ``dns.zone.Zone``. 1066 1067 *filename*, a ``str`` or ``None``, the filename to emit when 1068 describing where an error occurred; the default is ``'<string>'``. 1069 1070 *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE`` 1071 directives are permitted. If ``False``, then encoutering a ``$INCLUDE`` 1072 will raise a ``SyntaxError`` exception. 1073 1074 *check_origin*, a ``bool``. If ``True``, the default, then sanity 1075 checks of the origin node will be made by calling the zone's 1076 ``check_origin()`` method. 1077 1078 *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA 1079 encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder 1080 is used. 1081 1082 Raises ``dns.zone.NoSOA`` if there is no SOA RRset. 1083 1084 Raises ``dns.zone.NoNS`` if there is no NS RRset. 1085 1086 Raises ``KeyError`` if there is no origin node. 1087 1088 Returns a subclass of ``dns.zone.Zone``. 1089 """ 1090 1091 # 'text' can also be a file, but we don't publish that fact 1092 # since it's an implementation detail. The official file 1093 # interface is from_file(). 1094 1095 if filename is None: 1096 filename = '<string>' 1097 tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec) 1098 reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory, 1099 allow_include=allow_include, 1100 check_origin=check_origin) 1101 reader.read() 1102 return reader.zone 1103 1104 1105def from_file(f, origin=None, rdclass=dns.rdataclass.IN, 1106 relativize=True, zone_factory=Zone, filename=None, 1107 allow_include=True, check_origin=True): 1108 """Read a master file and build a zone object. 1109 1110 *f*, a file or ``str``. If *f* is a string, it is treated 1111 as the name of a file to open. 1112 1113 *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin 1114 of the zone; if not specified, the first ``$ORIGIN`` statement in the 1115 masterfile will determine the origin of the zone. 1116 1117 *rdclass*, an ``int``, the zone's rdata class; the default is class IN. 1118 1119 *relativize*, a ``bool``, determine's whether domain names are 1120 relativized to the zone's origin. The default is ``True``. 1121 1122 *zone_factory*, the zone factory to use or ``None``. If ``None``, then 1123 ``dns.zone.Zone`` will be used. The value may be any class or callable 1124 that returns a subclass of ``dns.zone.Zone``. 1125 1126 *filename*, a ``str`` or ``None``, the filename to emit when 1127 describing where an error occurred; the default is ``'<string>'``. 1128 1129 *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE`` 1130 directives are permitted. If ``False``, then encoutering a ``$INCLUDE`` 1131 will raise a ``SyntaxError`` exception. 1132 1133 *check_origin*, a ``bool``. If ``True``, the default, then sanity 1134 checks of the origin node will be made by calling the zone's 1135 ``check_origin()`` method. 1136 1137 *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA 1138 encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder 1139 is used. 1140 1141 Raises ``dns.zone.NoSOA`` if there is no SOA RRset. 1142 1143 Raises ``dns.zone.NoNS`` if there is no NS RRset. 1144 1145 Raises ``KeyError`` if there is no origin node. 1146 1147 Returns a subclass of ``dns.zone.Zone``. 1148 """ 1149 1150 with contextlib.ExitStack() as stack: 1151 if isinstance(f, str): 1152 if filename is None: 1153 filename = f 1154 f = stack.enter_context(open(f)) 1155 return from_text(f, origin, rdclass, relativize, zone_factory, 1156 filename, allow_include, check_origin) 1157 1158 1159def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True): 1160 """Convert the output of a zone transfer generator into a zone object. 1161 1162 *xfr*, a generator of ``dns.message.Message`` objects, typically 1163 ``dns.query.xfr()``. 1164 1165 *relativize*, a ``bool``, determine's whether domain names are 1166 relativized to the zone's origin. The default is ``True``. 1167 It is essential that the relativize setting matches the one specified 1168 to the generator. 1169 1170 *check_origin*, a ``bool``. If ``True``, the default, then sanity 1171 checks of the origin node will be made by calling the zone's 1172 ``check_origin()`` method. 1173 1174 Raises ``dns.zone.NoSOA`` if there is no SOA RRset. 1175 1176 Raises ``dns.zone.NoNS`` if there is no NS RRset. 1177 1178 Raises ``KeyError`` if there is no origin node. 1179 1180 Returns a subclass of ``dns.zone.Zone``. 1181 """ 1182 1183 z = None 1184 for r in xfr: 1185 if z is None: 1186 if relativize: 1187 origin = r.origin 1188 else: 1189 origin = r.answer[0].name 1190 rdclass = r.answer[0].rdclass 1191 z = zone_factory(origin, rdclass, relativize=relativize) 1192 for rrset in r.answer: 1193 znode = z.nodes.get(rrset.name) 1194 if not znode: 1195 znode = z.node_factory() 1196 z.nodes[rrset.name] = znode 1197 zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype, 1198 rrset.covers, True) 1199 zrds.update_ttl(rrset.ttl) 1200 for rd in rrset: 1201 zrds.add(rd) 1202 if check_origin: 1203 z.check_origin() 1204 return z 1205