Package dns :: Module zone
[hide private]
[frames] | no frames]

Source Code for Module dns.zone

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