zonefile.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  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. import re
  18. import sys
  19. import dns.exception
  20. import dns.name
  21. import dns.node
  22. import dns.rdataclass
  23. import dns.rdatatype
  24. import dns.rdata
  25. import dns.rdtypes.ANY.SOA
  26. import dns.rrset
  27. import dns.tokenizer
  28. import dns.transaction
  29. import dns.ttl
  30. import dns.grange
  31. class UnknownOrigin(dns.exception.DNSException):
  32. """Unknown origin"""
  33. class CNAMEAndOtherData(dns.exception.DNSException):
  34. """A node has a CNAME and other data"""
  35. def _check_cname_and_other_data(txn, name, rdataset):
  36. rdataset_kind = dns.node.NodeKind.classify_rdataset(rdataset)
  37. node = txn.get_node(name)
  38. if node is None:
  39. # empty nodes are neutral.
  40. return
  41. node_kind = node.classify()
  42. if node_kind == dns.node.NodeKind.CNAME and \
  43. rdataset_kind == dns.node.NodeKind.REGULAR:
  44. raise CNAMEAndOtherData('rdataset type is not compatible with a '
  45. 'CNAME node')
  46. elif node_kind == dns.node.NodeKind.REGULAR and \
  47. rdataset_kind == dns.node.NodeKind.CNAME:
  48. raise CNAMEAndOtherData('CNAME rdataset is not compatible with a '
  49. 'regular data node')
  50. # Otherwise at least one of the node and the rdataset is neutral, so
  51. # adding the rdataset is ok
  52. class Reader:
  53. """Read a DNS zone file into a transaction."""
  54. def __init__(self, tok, rdclass, txn, allow_include=False,
  55. allow_directives=True, force_name=None,
  56. force_ttl=None, force_rdclass=None, force_rdtype=None,
  57. default_ttl=None):
  58. self.tok = tok
  59. (self.zone_origin, self.relativize, _) = \
  60. txn.manager.origin_information()
  61. self.current_origin = self.zone_origin
  62. self.last_ttl = 0
  63. self.last_ttl_known = False
  64. if force_ttl is not None:
  65. default_ttl = force_ttl
  66. if default_ttl is None:
  67. self.default_ttl = 0
  68. self.default_ttl_known = False
  69. else:
  70. self.default_ttl = default_ttl
  71. self.default_ttl_known = True
  72. self.last_name = self.current_origin
  73. self.zone_rdclass = rdclass
  74. self.txn = txn
  75. self.saved_state = []
  76. self.current_file = None
  77. self.allow_include = allow_include
  78. self.allow_directives = allow_directives
  79. self.force_name = force_name
  80. self.force_ttl = force_ttl
  81. self.force_rdclass = force_rdclass
  82. self.force_rdtype = force_rdtype
  83. self.txn.check_put_rdataset(_check_cname_and_other_data)
  84. def _eat_line(self):
  85. while 1:
  86. token = self.tok.get()
  87. if token.is_eol_or_eof():
  88. break
  89. def _get_identifier(self):
  90. token = self.tok.get()
  91. if not token.is_identifier():
  92. raise dns.exception.SyntaxError
  93. return token
  94. def _rr_line(self):
  95. """Process one line from a DNS zone file."""
  96. token = None
  97. # Name
  98. if self.force_name is not None:
  99. name = self.force_name
  100. else:
  101. if self.current_origin is None:
  102. raise UnknownOrigin
  103. token = self.tok.get(want_leading=True)
  104. if not token.is_whitespace():
  105. self.last_name = self.tok.as_name(token, self.current_origin)
  106. else:
  107. token = self.tok.get()
  108. if token.is_eol_or_eof():
  109. # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
  110. return
  111. self.tok.unget(token)
  112. name = self.last_name
  113. if not name.is_subdomain(self.zone_origin):
  114. self._eat_line()
  115. return
  116. if self.relativize:
  117. name = name.relativize(self.zone_origin)
  118. # TTL
  119. if self.force_ttl is not None:
  120. ttl = self.force_ttl
  121. self.last_ttl = ttl
  122. self.last_ttl_known = True
  123. else:
  124. token = self._get_identifier()
  125. ttl = None
  126. try:
  127. ttl = dns.ttl.from_text(token.value)
  128. self.last_ttl = ttl
  129. self.last_ttl_known = True
  130. token = None
  131. except dns.ttl.BadTTL:
  132. if self.default_ttl_known:
  133. ttl = self.default_ttl
  134. elif self.last_ttl_known:
  135. ttl = self.last_ttl
  136. self.tok.unget(token)
  137. # Class
  138. if self.force_rdclass is not None:
  139. rdclass = self.force_rdclass
  140. else:
  141. token = self._get_identifier()
  142. try:
  143. rdclass = dns.rdataclass.from_text(token.value)
  144. except dns.exception.SyntaxError:
  145. raise
  146. except Exception:
  147. rdclass = self.zone_rdclass
  148. self.tok.unget(token)
  149. if rdclass != self.zone_rdclass:
  150. raise dns.exception.SyntaxError("RR class is not zone's class")
  151. # Type
  152. if self.force_rdtype is not None:
  153. rdtype = self.force_rdtype
  154. else:
  155. token = self._get_identifier()
  156. try:
  157. rdtype = dns.rdatatype.from_text(token.value)
  158. except Exception:
  159. raise dns.exception.SyntaxError(
  160. "unknown rdatatype '%s'" % token.value)
  161. try:
  162. rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
  163. self.current_origin, self.relativize,
  164. self.zone_origin)
  165. except dns.exception.SyntaxError:
  166. # Catch and reraise.
  167. raise
  168. except Exception:
  169. # All exceptions that occur in the processing of rdata
  170. # are treated as syntax errors. This is not strictly
  171. # correct, but it is correct almost all of the time.
  172. # We convert them to syntax errors so that we can emit
  173. # helpful filename:line info.
  174. (ty, va) = sys.exc_info()[:2]
  175. raise dns.exception.SyntaxError(
  176. "caught exception {}: {}".format(str(ty), str(va)))
  177. if not self.default_ttl_known and rdtype == dns.rdatatype.SOA:
  178. # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
  179. # TTL from the SOA minttl if no $TTL statement is present before the
  180. # SOA is parsed.
  181. self.default_ttl = rd.minimum
  182. self.default_ttl_known = True
  183. if ttl is None:
  184. # if we didn't have a TTL on the SOA, set it!
  185. ttl = rd.minimum
  186. # TTL check. We had to wait until now to do this as the SOA RR's
  187. # own TTL can be inferred from its minimum.
  188. if ttl is None:
  189. raise dns.exception.SyntaxError("Missing default TTL value")
  190. self.txn.add(name, ttl, rd)
  191. def _parse_modify(self, side):
  192. # Here we catch everything in '{' '}' in a group so we can replace it
  193. # with ''.
  194. is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
  195. is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$")
  196. is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$")
  197. # Sometimes there are modifiers in the hostname. These come after
  198. # the dollar sign. They are in the form: ${offset[,width[,base]]}.
  199. # Make names
  200. g1 = is_generate1.match(side)
  201. if g1:
  202. mod, sign, offset, width, base = g1.groups()
  203. if sign == '':
  204. sign = '+'
  205. g2 = is_generate2.match(side)
  206. if g2:
  207. mod, sign, offset = g2.groups()
  208. if sign == '':
  209. sign = '+'
  210. width = 0
  211. base = 'd'
  212. g3 = is_generate3.match(side)
  213. if g3:
  214. mod, sign, offset, width = g3.groups()
  215. if sign == '':
  216. sign = '+'
  217. base = 'd'
  218. if not (g1 or g2 or g3):
  219. mod = ''
  220. sign = '+'
  221. offset = 0
  222. width = 0
  223. base = 'd'
  224. if base != 'd':
  225. raise NotImplementedError()
  226. return mod, sign, offset, width, base
  227. def _generate_line(self):
  228. # range lhs [ttl] [class] type rhs [ comment ]
  229. """Process one line containing the GENERATE statement from a DNS
  230. zone file."""
  231. if self.current_origin is None:
  232. raise UnknownOrigin
  233. token = self.tok.get()
  234. # Range (required)
  235. try:
  236. start, stop, step = dns.grange.from_text(token.value)
  237. token = self.tok.get()
  238. if not token.is_identifier():
  239. raise dns.exception.SyntaxError
  240. except Exception:
  241. raise dns.exception.SyntaxError
  242. # lhs (required)
  243. try:
  244. lhs = token.value
  245. token = self.tok.get()
  246. if not token.is_identifier():
  247. raise dns.exception.SyntaxError
  248. except Exception:
  249. raise dns.exception.SyntaxError
  250. # TTL
  251. try:
  252. ttl = dns.ttl.from_text(token.value)
  253. self.last_ttl = ttl
  254. self.last_ttl_known = True
  255. token = self.tok.get()
  256. if not token.is_identifier():
  257. raise dns.exception.SyntaxError
  258. except dns.ttl.BadTTL:
  259. if not (self.last_ttl_known or self.default_ttl_known):
  260. raise dns.exception.SyntaxError("Missing default TTL value")
  261. if self.default_ttl_known:
  262. ttl = self.default_ttl
  263. elif self.last_ttl_known:
  264. ttl = self.last_ttl
  265. # Class
  266. try:
  267. rdclass = dns.rdataclass.from_text(token.value)
  268. token = self.tok.get()
  269. if not token.is_identifier():
  270. raise dns.exception.SyntaxError
  271. except dns.exception.SyntaxError:
  272. raise dns.exception.SyntaxError
  273. except Exception:
  274. rdclass = self.zone_rdclass
  275. if rdclass != self.zone_rdclass:
  276. raise dns.exception.SyntaxError("RR class is not zone's class")
  277. # Type
  278. try:
  279. rdtype = dns.rdatatype.from_text(token.value)
  280. token = self.tok.get()
  281. if not token.is_identifier():
  282. raise dns.exception.SyntaxError
  283. except Exception:
  284. raise dns.exception.SyntaxError("unknown rdatatype '%s'" %
  285. token.value)
  286. # rhs (required)
  287. rhs = token.value
  288. # The code currently only supports base 'd', so the last value
  289. # in the tuple _parse_modify returns is ignored
  290. lmod, lsign, loffset, lwidth, _ = self._parse_modify(lhs)
  291. rmod, rsign, roffset, rwidth, _ = self._parse_modify(rhs)
  292. for i in range(start, stop + 1, step):
  293. # +1 because bind is inclusive and python is exclusive
  294. if lsign == '+':
  295. lindex = i + int(loffset)
  296. elif lsign == '-':
  297. lindex = i - int(loffset)
  298. if rsign == '-':
  299. rindex = i - int(roffset)
  300. elif rsign == '+':
  301. rindex = i + int(roffset)
  302. lzfindex = str(lindex).zfill(int(lwidth))
  303. rzfindex = str(rindex).zfill(int(rwidth))
  304. name = lhs.replace('$%s' % (lmod), lzfindex)
  305. rdata = rhs.replace('$%s' % (rmod), rzfindex)
  306. self.last_name = dns.name.from_text(name, self.current_origin,
  307. self.tok.idna_codec)
  308. name = self.last_name
  309. if not name.is_subdomain(self.zone_origin):
  310. self._eat_line()
  311. return
  312. if self.relativize:
  313. name = name.relativize(self.zone_origin)
  314. try:
  315. rd = dns.rdata.from_text(rdclass, rdtype, rdata,
  316. self.current_origin, self.relativize,
  317. self.zone_origin)
  318. except dns.exception.SyntaxError:
  319. # Catch and reraise.
  320. raise
  321. except Exception:
  322. # All exceptions that occur in the processing of rdata
  323. # are treated as syntax errors. This is not strictly
  324. # correct, but it is correct almost all of the time.
  325. # We convert them to syntax errors so that we can emit
  326. # helpful filename:line info.
  327. (ty, va) = sys.exc_info()[:2]
  328. raise dns.exception.SyntaxError("caught exception %s: %s" %
  329. (str(ty), str(va)))
  330. self.txn.add(name, ttl, rd)
  331. def read(self):
  332. """Read a DNS zone file and build a zone object.
  333. @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
  334. @raises dns.zone.NoNS: No NS RRset was found at the zone origin
  335. """
  336. try:
  337. while 1:
  338. token = self.tok.get(True, True)
  339. if token.is_eof():
  340. if self.current_file is not None:
  341. self.current_file.close()
  342. if len(self.saved_state) > 0:
  343. (self.tok,
  344. self.current_origin,
  345. self.last_name,
  346. self.current_file,
  347. self.last_ttl,
  348. self.last_ttl_known,
  349. self.default_ttl,
  350. self.default_ttl_known) = self.saved_state.pop(-1)
  351. continue
  352. break
  353. elif token.is_eol():
  354. continue
  355. elif token.is_comment():
  356. self.tok.get_eol()
  357. continue
  358. elif token.value[0] == '$' and self.allow_directives:
  359. c = token.value.upper()
  360. if c == '$TTL':
  361. token = self.tok.get()
  362. if not token.is_identifier():
  363. raise dns.exception.SyntaxError("bad $TTL")
  364. self.default_ttl = dns.ttl.from_text(token.value)
  365. self.default_ttl_known = True
  366. self.tok.get_eol()
  367. elif c == '$ORIGIN':
  368. self.current_origin = self.tok.get_name()
  369. self.tok.get_eol()
  370. if self.zone_origin is None:
  371. self.zone_origin = self.current_origin
  372. self.txn._set_origin(self.current_origin)
  373. elif c == '$INCLUDE' and self.allow_include:
  374. token = self.tok.get()
  375. filename = token.value
  376. token = self.tok.get()
  377. if token.is_identifier():
  378. new_origin =\
  379. dns.name.from_text(token.value,
  380. self.current_origin,
  381. self.tok.idna_codec)
  382. self.tok.get_eol()
  383. elif not token.is_eol_or_eof():
  384. raise dns.exception.SyntaxError(
  385. "bad origin in $INCLUDE")
  386. else:
  387. new_origin = self.current_origin
  388. self.saved_state.append((self.tok,
  389. self.current_origin,
  390. self.last_name,
  391. self.current_file,
  392. self.last_ttl,
  393. self.last_ttl_known,
  394. self.default_ttl,
  395. self.default_ttl_known))
  396. self.current_file = open(filename, 'r')
  397. self.tok = dns.tokenizer.Tokenizer(self.current_file,
  398. filename)
  399. self.current_origin = new_origin
  400. elif c == '$GENERATE':
  401. self._generate_line()
  402. else:
  403. raise dns.exception.SyntaxError(
  404. "Unknown zone file directive '" + c + "'")
  405. continue
  406. self.tok.unget(token)
  407. self._rr_line()
  408. except dns.exception.SyntaxError as detail:
  409. (filename, line_number) = self.tok.where()
  410. if detail is None:
  411. detail = "syntax error"
  412. ex = dns.exception.SyntaxError(
  413. "%s:%d: %s" % (filename, line_number, detail))
  414. tb = sys.exc_info()[2]
  415. raise ex.with_traceback(tb) from None
  416. class RRsetsReaderTransaction(dns.transaction.Transaction):
  417. def __init__(self, manager, replacement, read_only):
  418. assert not read_only
  419. super().__init__(manager, replacement, read_only)
  420. self.rdatasets = {}
  421. def _get_rdataset(self, name, rdtype, covers):
  422. return self.rdatasets.get((name, rdtype, covers))
  423. def _get_node(self, name):
  424. rdatasets = []
  425. for (rdataset_name, _, _), rdataset in self.rdatasets.items():
  426. if name == rdataset_name:
  427. rdatasets.append(rdataset)
  428. if len(rdatasets) == 0:
  429. return None
  430. node = dns.node.Node()
  431. node.rdatasets = rdatasets
  432. return node
  433. def _put_rdataset(self, name, rdataset):
  434. self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset
  435. def _delete_name(self, name):
  436. # First remove any changes involving the name
  437. remove = []
  438. for key in self.rdatasets:
  439. if key[0] == name:
  440. remove.append(key)
  441. if len(remove) > 0:
  442. for key in remove:
  443. del self.rdatasets[key]
  444. def _delete_rdataset(self, name, rdtype, covers):
  445. try:
  446. del self.rdatasets[(name, rdtype, covers)]
  447. except KeyError:
  448. pass
  449. def _name_exists(self, name):
  450. for (n, _, _) in self.rdatasets:
  451. if n == name:
  452. return True
  453. return False
  454. def _changed(self):
  455. return len(self.rdatasets) > 0
  456. def _end_transaction(self, commit):
  457. if commit and self._changed():
  458. rrsets = []
  459. for (name, _, _), rdataset in self.rdatasets.items():
  460. rrset = dns.rrset.RRset(name, rdataset.rdclass, rdataset.rdtype,
  461. rdataset.covers)
  462. rrset.update(rdataset)
  463. rrsets.append(rrset)
  464. self.manager.set_rrsets(rrsets)
  465. def _set_origin(self, origin):
  466. pass
  467. class RRSetsReaderManager(dns.transaction.TransactionManager):
  468. def __init__(self, origin=dns.name.root, relativize=False,
  469. rdclass=dns.rdataclass.IN):
  470. self.origin = origin
  471. self.relativize = relativize
  472. self.rdclass = rdclass
  473. self.rrsets = []
  474. def writer(self, replacement=False):
  475. assert replacement is True
  476. return RRsetsReaderTransaction(self, True, False)
  477. def get_class(self):
  478. return self.rdclass
  479. def origin_information(self):
  480. if self.relativize:
  481. effective = dns.name.empty
  482. else:
  483. effective = self.origin
  484. return (self.origin, self.relativize, effective)
  485. def set_rrsets(self, rrsets):
  486. self.rrsets = rrsets
  487. def read_rrsets(text, name=None, ttl=None, rdclass=dns.rdataclass.IN,
  488. default_rdclass=dns.rdataclass.IN,
  489. rdtype=None, default_ttl=None, idna_codec=None,
  490. origin=dns.name.root, relativize=False):
  491. """Read one or more rrsets from the specified text, possibly subject
  492. to restrictions.
  493. *text*, a file object or a string, is the input to process.
  494. *name*, a string, ``dns.name.Name``, or ``None``, is the owner name of
  495. the rrset. If not ``None``, then the owner name is "forced", and the
  496. input must not specify an owner name. If ``None``, then any owner names
  497. are allowed and must be present in the input.
  498. *ttl*, an ``int``, string, or None. If not ``None``, the the TTL is
  499. forced to be the specified value and the input must not specify a TTL.
  500. If ``None``, then a TTL may be specified in the input. If it is not
  501. specified, then the *default_ttl* will be used.
  502. *rdclass*, a ``dns.rdataclass.RdataClass``, string, or ``None``. If
  503. not ``None``, then the class is forced to the specified value, and the
  504. input must not specify a class. If ``None``, then the input may specify
  505. a class that matches *default_rdclass*. Note that it is not possible to
  506. return rrsets with differing classes; specifying ``None`` for the class
  507. simply allows the user to optionally type a class as that may be convenient
  508. when cutting and pasting.
  509. *default_rdclass*, a ``dns.rdataclass.RdataClass`` or string. The class
  510. of the returned rrsets.
  511. *rdtype*, a ``dns.rdatatype.RdataType``, string, or ``None``. If not
  512. ``None``, then the type is forced to the specified value, and the
  513. input must not specify a type. If ``None``, then a type must be present
  514. for each RR.
  515. *default_ttl*, an ``int``, string, or ``None``. If not ``None``, then if
  516. the TTL is not forced and is not specified, then this value will be used.
  517. if ``None``, then if the TTL is not forced an error will occur if the TTL
  518. is not specified.
  519. *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
  520. encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
  521. is used. Note that codecs only apply to the owner name; dnspython does
  522. not do IDNA for names in rdata, as there is no IDNA zonefile format.
  523. *origin*, a string, ``dns.name.Name``, or ``None``, is the origin for any
  524. relative names in the input, and also the origin to relativize to if
  525. *relativize* is ``True``.
  526. *relativize*, a bool. If ``True``, names are relativized to the *origin*;
  527. if ``False`` then any relative names in the input are made absolute by
  528. appending the *origin*.
  529. """
  530. if isinstance(origin, str):
  531. origin = dns.name.from_text(origin, dns.name.root, idna_codec)
  532. if isinstance(name, str):
  533. name = dns.name.from_text(name, origin, idna_codec)
  534. if isinstance(ttl, str):
  535. ttl = dns.ttl.from_text(ttl)
  536. if isinstance(default_ttl, str):
  537. default_ttl = dns.ttl.from_text(default_ttl)
  538. if rdclass is not None:
  539. rdclass = dns.rdataclass.RdataClass.make(rdclass)
  540. default_rdclass = dns.rdataclass.RdataClass.make(default_rdclass)
  541. if rdtype is not None:
  542. rdtype = dns.rdatatype.RdataType.make(rdtype)
  543. manager = RRSetsReaderManager(origin, relativize, default_rdclass)
  544. with manager.writer(True) as txn:
  545. tok = dns.tokenizer.Tokenizer(text, '<input>', idna_codec=idna_codec)
  546. reader = Reader(tok, default_rdclass, txn, allow_directives=False,
  547. force_name=name, force_ttl=ttl, force_rdclass=rdclass,
  548. force_rdtype=rdtype, default_ttl=default_ttl)
  549. reader.read()
  550. return manager.rrsets