zone.py 39 KB

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