1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """DNS Zones."""
17
18 from __future__ import generators
19
20 import builtins
21 import sys
22
23 import dns.exception
24 import dns.name
25 import dns.node
26 import dns.rdataclass
27 import dns.rdatatype
28 import dns.rdata
29 import dns.rrset
30 import dns.tokenizer
31 import dns.ttl
32
33 -class BadZone(dns.exception.DNSException):
34 """The zone is malformed."""
35 pass
36
38 """The zone has no SOA RR at its origin."""
39 pass
40
42 """The zone has no NS RRset at its origin."""
43 pass
44
46 """The zone's origin is unknown."""
47 pass
48
50 """A DNS zone.
51
52 A Zone is a mapping from names to nodes. The zone object may be
53 treated like a Python dictionary, e.g. zone[name] will retrieve
54 the node associated with that name. The I{name} may be a
55 dns.name.Name object, or it may be a string. In the either case,
56 if the name is relative it is treated as relative to the origin of
57 the zone.
58
59 @ivar rdclass: The zone's rdata class; the default is class IN.
60 @type rdclass: int
61 @ivar origin: The origin of the zone.
62 @type origin: dns.name.Name object
63 @ivar nodes: A dictionary mapping the names of nodes in the zone to the
64 nodes themselves.
65 @type nodes: dict
66 @ivar relativize: should names in the zone be relativized?
67 @type relativize: bool
68 @cvar node_factory: the factory used to create a new node
69 @type node_factory: class or callable
70 """
71
72 node_factory = dns.node.Node
73
74 __slots__ = ['rdclass', 'origin', 'nodes', 'relativize']
75
77 """Initialize a zone object.
78
79 @param origin: The origin of the zone.
80 @type origin: dns.name.Name object
81 @param rdclass: The zone's rdata class; the default is class IN.
82 @type rdclass: int"""
83
84 self.rdclass = rdclass
85 self.origin = origin
86 self.nodes = {}
87 self.relativize = relativize
88
90 """Two zones are equal if they have the same origin, class, and
91 nodes.
92 @rtype: bool
93 """
94
95 if not isinstance(other, Zone):
96 return False
97 if self.rdclass != other.rdclass or \
98 self.origin != other.origin or \
99 self.nodes != other.nodes:
100 return False
101 return True
102
104 """Are two zones not equal?
105 @rtype: bool
106 """
107
108 return not self.__eq__(other)
109
121
125
129
133
135 return iter(self.nodes.keys())
136
138 return self.nodes.keys()
139
141 return self.nodes.values()
142
144 return self.nodes.items()
145
146 - def get(self, key):
149
151 return other in self.nodes
152
154 """Find a node in the zone, possibly creating it.
155
156 @param name: the name of the node to find
157 @type name: dns.name.Name object or string
158 @param create: should the node be created if it doesn't exist?
159 @type create: bool
160 @raises KeyError: the name is not known and create was not specified.
161 @rtype: dns.node.Node object
162 """
163
164 name = self._validate_name(name)
165 node = self.nodes.get(name)
166 if node is None:
167 if not create:
168 raise KeyError
169 node = self.node_factory()
170 self.nodes[name] = node
171 return node
172
173 - def get_node(self, name, create=False):
174 """Get a node in the zone, possibly creating it.
175
176 This method is like L{find_node}, except it returns None instead
177 of raising an exception if the node does not exist and creation
178 has not been requested.
179
180 @param name: the name of the node to find
181 @type name: dns.name.Name object or string
182 @param create: should the node be created if it doesn't exist?
183 @type create: bool
184 @rtype: dns.node.Node object or None
185 """
186
187 try:
188 node = self.find_node(name, create)
189 except KeyError:
190 node = None
191 return node
192
194 """Delete the specified node if it exists.
195
196 It is not an error if the node does not exist.
197 """
198
199 name = self._validate_name(name)
200 if name in self.nodes:
201 del self.nodes[name]
202
205 """Look for rdata with the specified name and type in the zone,
206 and return an rdataset encapsulating it.
207
208 The I{name}, I{rdtype}, and I{covers} parameters may be
209 strings, in which case they will be converted to their proper
210 type.
211
212 The rdataset returned is not a copy; changes to it will change
213 the zone.
214
215 KeyError is raised if the name or type are not found.
216 Use L{get_rdataset} if you want to have None returned instead.
217
218 @param name: the owner name to look for
219 @type name: DNS.name.Name object or string
220 @param rdtype: the rdata type desired
221 @type rdtype: int or string
222 @param covers: the covered type (defaults to None)
223 @type covers: int or string
224 @param create: should the node and rdataset be created if they do not
225 exist?
226 @type create: bool
227 @raises KeyError: the node or rdata could not be found
228 @rtype: dns.rrset.RRset object
229 """
230
231 name = self._validate_name(name)
232 if isinstance(rdtype, str):
233 rdtype = dns.rdatatype.from_text(rdtype)
234 if isinstance(covers, str):
235 covers = dns.rdatatype.from_text(covers)
236 node = self.find_node(name, create)
237 return node.find_rdataset(self.rdclass, rdtype, covers, create)
238
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 None is returned if the name or type are not found.
252 Use L{find_rdataset} if you want to have KeyError raised 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 @rtype: dns.rrset.RRset object
264 """
265
266 try:
267 rdataset = self.find_rdataset(name, rdtype, covers, create)
268 except KeyError:
269 rdataset = None
270 return rdataset
271
273 """Delete the rdataset matching I{rdtype} and I{covers}, if it
274 exists at the node specified by I{name}.
275
276 The I{name}, I{rdtype}, and I{covers} parameters may be
277 strings, in which case they will be converted to their proper
278 type.
279
280 It is not an error if the node does not exist, or if there is no
281 matching rdataset at the node.
282
283 If the node has no rdatasets after the deletion, it will itself
284 be deleted.
285
286 @param name: the owner name to look for
287 @type name: DNS.name.Name object or string
288 @param rdtype: the rdata type desired
289 @type rdtype: int or string
290 @param covers: the covered type (defaults to None)
291 @type covers: int or string
292 """
293
294 name = self._validate_name(name)
295 if isinstance(rdtype, str):
296 rdtype = dns.rdatatype.from_text(rdtype)
297 if isinstance(covers, str):
298 covers = dns.rdatatype.from_text(covers)
299 node = self.get_node(name)
300 if not node is None:
301 node.delete_rdataset(self.rdclass, rdtype, covers)
302 if len(node) == 0:
303 self.delete_node(name)
304
306 """Replace an rdataset at name.
307
308 It is not an error if there is no rdataset matching I{replacement}.
309
310 Ownership of the I{replacement} object is transferred to the zone;
311 in other words, this method does not store a copy of I{replacement}
312 at the node, it stores I{replacement} itself.
313
314 If the I{name} node does not exist, it is created.
315
316 @param name: the owner name
317 @type name: DNS.name.Name object or string
318 @param replacement: the replacement rdataset
319 @type replacement: dns.rdataset.Rdataset
320 """
321
322 if replacement.rdclass != self.rdclass:
323 raise ValueError('replacement.rdclass != zone.rdclass')
324 node = self.find_node(name, True)
325 node.replace_rdataset(replacement)
326
328 """Look for rdata with the specified name and type in the zone,
329 and return an RRset encapsulating it.
330
331 The I{name}, I{rdtype}, and I{covers} parameters may be
332 strings, in which case they will be converted to their proper
333 type.
334
335 This method is less efficient than the similar
336 L{find_rdataset} because it creates an RRset instead of
337 returning the matching rdataset. It may be more convenient
338 for some uses since it returns an object which binds the owner
339 name to the rdata.
340
341 This method may not be used to create new nodes or rdatasets;
342 use L{find_rdataset} instead.
343
344 KeyError is raised if the name or type are not found.
345 Use L{get_rrset} if you want to have None returned instead.
346
347 @param name: the owner name to look for
348 @type name: DNS.name.Name object or string
349 @param rdtype: the rdata type desired
350 @type rdtype: int or string
351 @param covers: the covered type (defaults to None)
352 @type covers: int or string
353 @raises KeyError: the node or rdata could not be found
354 @rtype: dns.rrset.RRset object
355 """
356
357 name = self._validate_name(name)
358 if isinstance(rdtype, str):
359 rdtype = dns.rdatatype.from_text(rdtype)
360 if isinstance(covers, str):
361 covers = dns.rdatatype.from_text(covers)
362 rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
363 rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
364 rrset.update(rdataset)
365 return rrset
366
368 """Look for rdata with the specified name and type in the zone,
369 and return an RRset encapsulating it.
370
371 The I{name}, I{rdtype}, and I{covers} parameters may be
372 strings, in which case they will be converted to their proper
373 type.
374
375 This method is less efficient than the similar L{get_rdataset}
376 because it creates an RRset instead of returning the matching
377 rdataset. It may be more convenient for some uses since it
378 returns an object which binds the owner name to the rdata.
379
380 This method may not be used to create new nodes or rdatasets;
381 use L{find_rdataset} instead.
382
383 None is returned if the name or type are not found.
384 Use L{find_rrset} if you want to have KeyError raised instead.
385
386 @param name: the owner name to look for
387 @type name: DNS.name.Name object or string
388 @param rdtype: the rdata type desired
389 @type rdtype: int or string
390 @param covers: the covered type (defaults to None)
391 @type covers: int or string
392 @rtype: dns.rrset.RRset object
393 """
394
395 try:
396 rrset = self.find_rrset(name, rdtype, covers)
397 except KeyError:
398 rrset = None
399 return rrset
400
403 """Return a generator which yields (name, rdataset) tuples for
404 all rdatasets in the zone which have the specified I{rdtype}
405 and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default,
406 then all rdatasets will be matched.
407
408 @param rdtype: int or string
409 @type rdtype: int or string
410 @param covers: the covered type (defaults to None)
411 @type covers: int or string
412 """
413
414 if isinstance(rdtype, str):
415 rdtype = dns.rdatatype.from_text(rdtype)
416 if isinstance(covers, str):
417 covers = dns.rdatatype.from_text(covers)
418 for (name, node) in self.items():
419 for rds in node:
420 if rdtype == dns.rdatatype.ANY or \
421 (rds.rdtype == rdtype and rds.covers == covers):
422 yield (name, rds)
423
447
448 - def to_file(self, f, sorted=True, relativize=True, nl=None):
449 """Write a zone to a file.
450
451 @param f: file or string. If I{f} is a string, it is treated
452 as the name of a file to open.
453 @param sorted: if True, the file will be written with the
454 names sorted in DNSSEC order from least to greatest. Otherwise
455 the names will be written in whatever order they happen to have
456 in the zone's dictionary.
457 @param relativize: if True, domain names in the output will be
458 relativized to the zone's origin (if possible).
459 @type relativize: bool
460 @param nl: The end of line string. If not specified, the
461 output will use the platform's native end-of-line marker (i.e.
462 LF on POSIX, CRLF on Windows, CR on Macintosh).
463 @type nl: string or None
464 """
465
466 if nl is None:
467 opts = 'w'
468 else:
469 opts = 'wb'
470 if isinstance(f, str):
471 f = open(f, opts)
472 want_close = True
473 else:
474 want_close = False
475 try:
476 if sorted:
477 names = builtins.sorted(self.keys())
478 else:
479 names = self.keys()
480 for n in names:
481 l = self[n].to_text(n, origin=self.origin,
482 relativize=relativize)
483 if nl is None:
484 print(l, file=f)
485 else:
486 f.write(l.encode('ascii'))
487 f.write(nl.encode('ascii'))
488 finally:
489 if want_close:
490 f.close()
491
507
508
510 """Read a DNS master file
511
512 @ivar tok: The tokenizer
513 @type tok: dns.tokenizer.Tokenizer object
514 @ivar ttl: The default TTL
515 @type ttl: int
516 @ivar last_name: The last name read
517 @type last_name: dns.name.Name object
518 @ivar current_origin: The current origin
519 @type current_origin: dns.name.Name object
520 @ivar relativize: should names in the zone be relativized?
521 @type relativize: bool
522 @ivar zone: the zone
523 @type zone: dns.zone.Zone object
524 @ivar saved_state: saved reader state (used when processing $INCLUDE)
525 @type saved_state: list of (tokenizer, current_origin, last_name, file)
526 tuples.
527 @ivar current_file: the file object of the $INCLUDed file being parsed
528 (None if no $INCLUDE is active).
529 @ivar allow_include: is $INCLUDE allowed?
530 @type allow_include: bool
531 @ivar check_origin: should sanity checks of the origin node be done?
532 The default is True.
533 @type check_origin: bool
534 """
535
536 - def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
537 allow_include=False, check_origin=True):
550
556
558 """Process one line from a DNS master file."""
559
560 if self.current_origin is None:
561 raise UnknownOrigin
562 token = self.tok.get(want_leading = True)
563 if not token.is_whitespace():
564 self.last_name = dns.name.from_text(token.value, self.current_origin)
565 else:
566 token = self.tok.get()
567 if token.is_eol_or_eof():
568
569 return
570 self.tok.unget(token)
571 name = self.last_name
572 if not name.is_subdomain(self.zone.origin):
573 self._eat_line()
574 return
575 if self.relativize:
576 name = name.relativize(self.zone.origin)
577 token = self.tok.get()
578 if not token.is_identifier():
579 raise dns.exception.SyntaxError
580
581 try:
582 ttl = dns.ttl.from_text(token.value)
583 token = self.tok.get()
584 if not token.is_identifier():
585 raise dns.exception.SyntaxError
586 except dns.ttl.BadTTL:
587 ttl = self.ttl
588
589 try:
590 rdclass = dns.rdataclass.from_text(token.value)
591 token = self.tok.get()
592 if not token.is_identifier():
593 raise dns.exception.SyntaxError
594 except dns.exception.SyntaxError:
595 raise dns.exception.SyntaxError
596 except:
597 rdclass = self.zone.rdclass
598 if rdclass != self.zone.rdclass:
599 raise dns.exception.SyntaxError("RR class is not zone's class")
600
601 try:
602 rdtype = dns.rdatatype.from_text(token.value)
603 except:
604 raise dns.exception.SyntaxError("unknown rdatatype '%s'" % token.value)
605 n = self.zone.nodes.get(name)
606 if n is None:
607 n = self.zone.node_factory()
608 self.zone.nodes[name] = n
609 try:
610 rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
611 self.current_origin, False)
612 except dns.exception.SyntaxError:
613
614 (ty, va) = sys.exc_info()[:2]
615 raise va
616 except:
617
618
619
620
621
622 (ty, va) = sys.exc_info()[:2]
623 raise dns.exception.SyntaxError("caught exception %s: %s" % (str(ty), str(va)))
624
625 rd.choose_relativity(self.zone.origin, self.relativize)
626 covers = rd.covers()
627 rds = n.find_rdataset(rdclass, rdtype, covers, True)
628 rds.add(rd, ttl)
629
631 """Read a DNS master file and build a zone object.
632
633 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
634 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
635 """
636
637 try:
638 while 1:
639 token = self.tok.get(True, True).unescape()
640 if token.is_eof():
641 if not self.current_file is None:
642 self.current_file.close()
643 if len(self.saved_state) > 0:
644 (self.tok,
645 self.current_origin,
646 self.last_name,
647 self.current_file,
648 self.ttl) = self.saved_state.pop(-1)
649 continue
650 break
651 elif token.is_eol():
652 continue
653 elif token.is_comment():
654 self.tok.get_eol()
655 continue
656 elif token.value[0] == '$':
657 u = token.value.upper()
658 if u == '$TTL':
659 token = self.tok.get()
660 if not token.is_identifier():
661 raise dns.exception.SyntaxError("bad $TTL")
662 self.ttl = dns.ttl.from_text(token.value)
663 self.tok.get_eol()
664 elif u == '$ORIGIN':
665 self.current_origin = self.tok.get_name()
666 self.tok.get_eol()
667 if self.zone.origin is None:
668 self.zone.origin = self.current_origin
669 elif u == '$INCLUDE' and self.allow_include:
670 token = self.tok.get()
671 if not token.is_quoted_string():
672 raise dns.exception.SyntaxError("bad filename in $INCLUDE")
673 filename = token.value
674 token = self.tok.get()
675 if token.is_identifier():
676 new_origin = dns.name.from_text(token.value, \
677 self.current_origin)
678 self.tok.get_eol()
679 elif not token.is_eol_or_eof():
680 raise dns.exception.SyntaxError("bad origin in $INCLUDE")
681 else:
682 new_origin = self.current_origin
683 self.saved_state.append((self.tok,
684 self.current_origin,
685 self.last_name,
686 self.current_file,
687 self.ttl))
688 self.current_file = open(filename, 'r')
689 self.tok = dns.tokenizer.Tokenizer(self.current_file,
690 filename)
691 self.current_origin = new_origin
692 else:
693 raise dns.exception.SyntaxError("Unknown master file directive '" + u + "'")
694 continue
695 self.tok.unget(token)
696 self._rr_line()
697 except dns.exception.SyntaxError as detail:
698 (filename, line_number) = self.tok.where()
699 if detail is None:
700 detail = "syntax error"
701 raise dns.exception.SyntaxError("%s:%d: %s" % (filename, line_number, detail))
702
703
704 if self.check_origin:
705 self.zone.check_origin()
706
707 -def from_text(text, origin = None, rdclass = dns.rdataclass.IN,
708 relativize = True, zone_factory=Zone, filename=None,
709 allow_include=False, check_origin=True):
710 """Build a zone object from a master file format string.
711
712 @param text: the master file format input
713 @type text: string.
714 @param origin: The origin of the zone; if not specified, the first
715 $ORIGIN statement in the master file will determine the origin of the
716 zone.
717 @type origin: dns.name.Name object or string
718 @param rdclass: The zone's rdata class; the default is class IN.
719 @type rdclass: int
720 @param relativize: should names be relativized? The default is True
721 @type relativize: bool
722 @param zone_factory: The zone factory to use
723 @type zone_factory: function returning a Zone
724 @param filename: The filename to emit when describing where an error
725 occurred; the default is '<string>'.
726 @type filename: string
727 @param allow_include: is $INCLUDE allowed?
728 @type allow_include: bool
729 @param check_origin: should sanity checks of the origin node be done?
730 The default is True.
731 @type check_origin: bool
732 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
733 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
734 @rtype: dns.zone.Zone object
735 """
736
737
738
739
740
741 if filename is None:
742 filename = '<string>'
743 tok = dns.tokenizer.Tokenizer(text, filename)
744 reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
745 allow_include=allow_include,
746 check_origin=check_origin)
747 reader.read()
748 return reader.zone
749
750 -def from_file(f, origin = None, rdclass = dns.rdataclass.IN,
751 relativize = True, zone_factory=Zone, filename=None,
752 allow_include=True, check_origin=True):
753 """Read a master file and build a zone object.
754
755 @param f: file or string. If I{f} is a string, it is treated
756 as the name of a file to open.
757 @param origin: The origin of the zone; if not specified, the first
758 $ORIGIN statement in the master file will determine the origin of the
759 zone.
760 @type origin: dns.name.Name object or string
761 @param rdclass: The zone's rdata class; the default is class IN.
762 @type rdclass: int
763 @param relativize: should names be relativized? The default is True
764 @type relativize: bool
765 @param zone_factory: The zone factory to use
766 @type zone_factory: function returning a Zone
767 @param filename: The filename to emit when describing where an error
768 occurred; the default is '<file>', or the value of I{f} if I{f} is a
769 string.
770 @type filename: string
771 @param allow_include: is $INCLUDE allowed?
772 @type allow_include: bool
773 @param check_origin: should sanity checks of the origin node be done?
774 The default is True.
775 @type check_origin: bool
776 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
777 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
778 @rtype: dns.zone.Zone object
779 """
780
781 if isinstance(f, str):
782 if filename is None:
783 filename = f
784 f = open(f, 'rU')
785 want_close = True
786 else:
787 if filename is None:
788 filename = '<file>'
789 want_close = False
790
791 try:
792 z = from_text(f, origin, rdclass, relativize, zone_factory,
793 filename, allow_include, check_origin)
794 finally:
795 if want_close:
796 f.close()
797 return z
798
799 -def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
800 """Convert the output of a zone transfer generator into a zone object.
801
802 @param xfr: The xfr generator
803 @type xfr: generator of dns.message.Message objects
804 @param relativize: should names be relativized? The default is True.
805 It is essential that the relativize setting matches the one specified
806 to dns.query.xfr().
807 @type relativize: bool
808 @param check_origin: should sanity checks of the origin node be done?
809 The default is True.
810 @type check_origin: bool
811 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
812 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
813 @rtype: dns.zone.Zone object
814 """
815
816 z = None
817 for r in xfr:
818 if z is None:
819 if relativize:
820 origin = r.origin
821 else:
822 origin = r.answer[0].name
823 rdclass = r.answer[0].rdclass
824 z = zone_factory(origin, rdclass, relativize=relativize)
825 for rrset in r.answer:
826 znode = z.nodes.get(rrset.name)
827 if not znode:
828 znode = z.node_factory()
829 z.nodes[rrset.name] = znode
830 zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
831 rrset.covers, True)
832 zrds.update_ttl(rrset.ttl)
833 for rd in rrset:
834 rd.choose_relativity(z.origin, relativize)
835 zrds.add(rd)
836 if check_origin:
837 z.check_origin()
838 return z
839