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