You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1127 lines
41 KiB

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