zone.py 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462
  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 contextlib
  18. import io
  19. import os
  20. import struct
  21. from typing import (
  22. Any,
  23. Callable,
  24. Iterable,
  25. Iterator,
  26. List,
  27. MutableMapping,
  28. Set,
  29. Tuple,
  30. cast,
  31. )
  32. import dns.exception
  33. import dns.grange
  34. import dns.immutable
  35. import dns.name
  36. import dns.node
  37. import dns.rdata
  38. import dns.rdataclass
  39. import dns.rdataset
  40. import dns.rdatatype
  41. import dns.rdtypes.ANY.SOA
  42. import dns.rdtypes.ANY.ZONEMD
  43. import dns.rrset
  44. import dns.tokenizer
  45. import dns.transaction
  46. import dns.ttl
  47. import dns.zonefile
  48. from dns.zonetypes import DigestHashAlgorithm, DigestScheme, _digest_hashers
  49. class BadZone(dns.exception.DNSException):
  50. """The DNS zone is malformed."""
  51. class NoSOA(BadZone):
  52. """The DNS zone has no SOA RR at its origin."""
  53. class NoNS(BadZone):
  54. """The DNS zone has no NS RRset at its origin."""
  55. class UnknownOrigin(BadZone):
  56. """The DNS zone's origin is unknown."""
  57. class UnsupportedDigestScheme(dns.exception.DNSException):
  58. """The zone digest's scheme is unsupported."""
  59. class UnsupportedDigestHashAlgorithm(dns.exception.DNSException):
  60. """The zone digest's origin is unsupported."""
  61. class NoDigest(dns.exception.DNSException):
  62. """The DNS zone has no ZONEMD RRset at its origin."""
  63. class DigestVerificationFailure(dns.exception.DNSException):
  64. """The ZONEMD digest failed to verify."""
  65. def _validate_name(
  66. name: dns.name.Name,
  67. origin: dns.name.Name | None,
  68. relativize: bool,
  69. ) -> dns.name.Name:
  70. # This name validation code is shared by Zone and Version
  71. if origin is None:
  72. # This should probably never happen as other code (e.g.
  73. # _rr_line) will notice the lack of an origin before us, but
  74. # we check just in case!
  75. raise KeyError("no zone origin is defined")
  76. if name.is_absolute():
  77. if not name.is_subdomain(origin):
  78. raise KeyError("name parameter must be a subdomain of the zone origin")
  79. if relativize:
  80. name = name.relativize(origin)
  81. else:
  82. # We have a relative name. Make sure that the derelativized name is
  83. # not too long.
  84. try:
  85. abs_name = name.derelativize(origin)
  86. except dns.name.NameTooLong:
  87. # We map dns.name.NameTooLong to KeyError to be consistent with
  88. # the other exceptions above.
  89. raise KeyError("relative name too long for zone")
  90. if not relativize:
  91. # We have a relative name in a non-relative zone, so use the
  92. # derelativized name.
  93. name = abs_name
  94. return name
  95. class Zone(dns.transaction.TransactionManager):
  96. """A DNS zone.
  97. A ``Zone`` is a mapping from names to nodes. The zone object may be
  98. treated like a Python dictionary, e.g. ``zone[name]`` will retrieve
  99. the node associated with that name. The *name* may be a
  100. ``dns.name.Name object``, or it may be a string. In either case,
  101. if the name is relative it is treated as relative to the origin of
  102. the zone.
  103. """
  104. node_factory: Callable[[], dns.node.Node] = dns.node.Node
  105. map_factory: Callable[[], MutableMapping[dns.name.Name, dns.node.Node]] = dict
  106. # We only require the version types as "Version" to allow for flexibility, as
  107. # only the version protocol matters
  108. writable_version_factory: Callable[["Zone", bool], "Version"] | None = None
  109. immutable_version_factory: Callable[["Version"], "Version"] | None = None
  110. __slots__ = ["rdclass", "origin", "nodes", "relativize"]
  111. def __init__(
  112. self,
  113. origin: dns.name.Name | str | None,
  114. rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
  115. relativize: bool = True,
  116. ):
  117. """Initialize a zone object.
  118. *origin* is the origin of the zone. It may be a ``dns.name.Name``,
  119. a ``str``, or ``None``. If ``None``, then the zone's origin will
  120. be set by the first ``$ORIGIN`` line in a zone file.
  121. *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
  122. *relativize*, a ``bool``, determine's whether domain names are
  123. relativized to the zone's origin. The default is ``True``.
  124. """
  125. if origin is not None:
  126. if isinstance(origin, str):
  127. origin = dns.name.from_text(origin)
  128. elif not isinstance(origin, dns.name.Name):
  129. raise ValueError("origin parameter must be convertible to a DNS name")
  130. if not origin.is_absolute():
  131. raise ValueError("origin parameter must be an absolute name")
  132. self.origin = origin
  133. self.rdclass = rdclass
  134. self.nodes: MutableMapping[dns.name.Name, dns.node.Node] = self.map_factory()
  135. self.relativize = relativize
  136. def __eq__(self, other):
  137. """Two zones are equal if they have the same origin, class, and
  138. nodes.
  139. Returns a ``bool``.
  140. """
  141. if not isinstance(other, Zone):
  142. return False
  143. if (
  144. self.rdclass != other.rdclass
  145. or self.origin != other.origin
  146. or self.nodes != other.nodes
  147. ):
  148. return False
  149. return True
  150. def __ne__(self, other):
  151. """Are two zones not equal?
  152. Returns a ``bool``.
  153. """
  154. return not self.__eq__(other)
  155. def _validate_name(self, name: dns.name.Name | str) -> dns.name.Name:
  156. # Note that any changes in this method should have corresponding changes
  157. # made in the Version _validate_name() method.
  158. if isinstance(name, str):
  159. name = dns.name.from_text(name, None)
  160. elif not isinstance(name, dns.name.Name):
  161. raise KeyError("name parameter must be convertible to a DNS name")
  162. return _validate_name(name, self.origin, self.relativize)
  163. def __getitem__(self, key):
  164. key = self._validate_name(key)
  165. return self.nodes[key]
  166. def __setitem__(self, key, value):
  167. key = self._validate_name(key)
  168. self.nodes[key] = value
  169. def __delitem__(self, key):
  170. key = self._validate_name(key)
  171. del self.nodes[key]
  172. def __iter__(self):
  173. return self.nodes.__iter__()
  174. def keys(self):
  175. return self.nodes.keys()
  176. def values(self):
  177. return self.nodes.values()
  178. def items(self):
  179. return self.nodes.items()
  180. def get(self, key):
  181. key = self._validate_name(key)
  182. return self.nodes.get(key)
  183. def __contains__(self, key):
  184. key = self._validate_name(key)
  185. return key in self.nodes
  186. def find_node(
  187. self, name: dns.name.Name | str, create: bool = False
  188. ) -> dns.node.Node:
  189. """Find a node in the zone, possibly creating it.
  190. *name*: the name of the node to find.
  191. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  192. name must be a subdomain of the zone's origin. If ``zone.relativize``
  193. is ``True``, then the name will be relativized.
  194. *create*, a ``bool``. If true, the node will be created if it does
  195. not exist.
  196. Raises ``KeyError`` if the name is not known and create was
  197. not specified, or if the name was not a subdomain of the origin.
  198. Returns a ``dns.node.Node``.
  199. """
  200. name = self._validate_name(name)
  201. node = self.nodes.get(name)
  202. if node is None:
  203. if not create:
  204. raise KeyError
  205. node = self.node_factory()
  206. self.nodes[name] = node
  207. return node
  208. def get_node(
  209. self, name: dns.name.Name | str, create: bool = False
  210. ) -> dns.node.Node | None:
  211. """Get a node in the zone, possibly creating it.
  212. This method is like ``find_node()``, except it returns None instead
  213. of raising an exception if the node does not exist and creation
  214. has not been requested.
  215. *name*: the name of the node to find.
  216. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  217. name must be a subdomain of the zone's origin. If ``zone.relativize``
  218. is ``True``, then the name will be relativized.
  219. *create*, a ``bool``. If true, the node will be created if it does
  220. not exist.
  221. Returns a ``dns.node.Node`` or ``None``.
  222. """
  223. try:
  224. node = self.find_node(name, create)
  225. except KeyError:
  226. node = None
  227. return node
  228. def delete_node(self, name: dns.name.Name | str) -> None:
  229. """Delete the specified node if it exists.
  230. *name*: the name of the node to find.
  231. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  232. name must be a subdomain of the zone's origin. If ``zone.relativize``
  233. is ``True``, then the name will be relativized.
  234. It is not an error if the node does not exist.
  235. """
  236. name = self._validate_name(name)
  237. if name in self.nodes:
  238. del self.nodes[name]
  239. def find_rdataset(
  240. self,
  241. name: dns.name.Name | str,
  242. rdtype: dns.rdatatype.RdataType | str,
  243. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  244. create: bool = False,
  245. ) -> dns.rdataset.Rdataset:
  246. """Look for an rdataset with the specified name and type in the zone,
  247. and return an rdataset encapsulating it.
  248. The rdataset returned is not a copy; changes to it will change
  249. the zone.
  250. KeyError is raised if the name or type are not found.
  251. *name*: the name of the node to find.
  252. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  253. name must be a subdomain of the zone's origin. If ``zone.relativize``
  254. is ``True``, then the name will be relativized.
  255. *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
  256. *covers*, a ``dns.rdatatype.RdataType`` or ``str`` the covered type.
  257. Usually this value is ``dns.rdatatype.NONE``, but if the
  258. rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
  259. then the covers value will be the rdata type the SIG/RRSIG
  260. covers. The library treats the SIG and RRSIG types as if they
  261. were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
  262. This makes RRSIGs much easier to work with than if RRSIGs
  263. covering different rdata types were aggregated into a single
  264. RRSIG rdataset.
  265. *create*, a ``bool``. If true, the node will be created if it does
  266. not exist.
  267. Raises ``KeyError`` if the name is not known and create was
  268. not specified, or if the name was not a subdomain of the origin.
  269. Returns a ``dns.rdataset.Rdataset``.
  270. """
  271. name = self._validate_name(name)
  272. rdtype = dns.rdatatype.RdataType.make(rdtype)
  273. covers = dns.rdatatype.RdataType.make(covers)
  274. node = self.find_node(name, create)
  275. return node.find_rdataset(self.rdclass, rdtype, covers, create)
  276. def get_rdataset(
  277. self,
  278. name: dns.name.Name | str,
  279. rdtype: dns.rdatatype.RdataType | str,
  280. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  281. create: bool = False,
  282. ) -> dns.rdataset.Rdataset | None:
  283. """Look for an rdataset with the specified name and type in the zone.
  284. This method is like ``find_rdataset()``, except it returns None instead
  285. of raising an exception if the rdataset does not exist and creation
  286. has not been requested.
  287. The rdataset returned is not a copy; changes to it will change
  288. the zone.
  289. *name*: the name of the node to find.
  290. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  291. name must be a subdomain of the zone's origin. If ``zone.relativize``
  292. is ``True``, then the name will be relativized.
  293. *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
  294. *covers*, a ``dns.rdatatype.RdataType`` or ``str``, the covered type.
  295. Usually this value is ``dns.rdatatype.NONE``, but if the
  296. rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
  297. then the covers value will be the rdata type the SIG/RRSIG
  298. covers. The library treats the SIG and RRSIG types as if they
  299. were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
  300. This makes RRSIGs much easier to work with than if RRSIGs
  301. covering different rdata types were aggregated into a single
  302. RRSIG rdataset.
  303. *create*, a ``bool``. If true, the node will be created if it does
  304. not exist.
  305. Raises ``KeyError`` if the name is not known and create was
  306. not specified, or if the name was not a subdomain of the origin.
  307. Returns a ``dns.rdataset.Rdataset`` or ``None``.
  308. """
  309. try:
  310. rdataset = self.find_rdataset(name, rdtype, covers, create)
  311. except KeyError:
  312. rdataset = None
  313. return rdataset
  314. def delete_rdataset(
  315. self,
  316. name: dns.name.Name | str,
  317. rdtype: dns.rdatatype.RdataType | str,
  318. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  319. ) -> None:
  320. """Delete the rdataset matching *rdtype* and *covers*, if it
  321. exists at the node specified by *name*.
  322. It is not an error if the node does not exist, or if there is no matching
  323. rdataset at the node.
  324. If the node has no rdatasets after the deletion, it will itself be deleted.
  325. *name*: the name of the node to find. The value may be a ``dns.name.Name`` or a
  326. ``str``. If absolute, the name must be a subdomain of the zone's origin. If
  327. ``zone.relativize`` is ``True``, then the name will be relativized.
  328. *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
  329. *covers*, a ``dns.rdatatype.RdataType`` or ``str`` or ``None``, the covered
  330. type. Usually this value is ``dns.rdatatype.NONE``, but if the rdtype is
  331. ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, then the covers value will be
  332. the rdata type the SIG/RRSIG covers. The library treats the SIG and RRSIG types
  333. as if they were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This
  334. makes RRSIGs much easier to work with than if RRSIGs covering different rdata
  335. types were aggregated into a single RRSIG rdataset.
  336. """
  337. name = self._validate_name(name)
  338. rdtype = dns.rdatatype.RdataType.make(rdtype)
  339. covers = dns.rdatatype.RdataType.make(covers)
  340. node = self.get_node(name)
  341. if node is not None:
  342. node.delete_rdataset(self.rdclass, rdtype, covers)
  343. if len(node) == 0:
  344. self.delete_node(name)
  345. def replace_rdataset(
  346. self, name: dns.name.Name | str, replacement: dns.rdataset.Rdataset
  347. ) -> None:
  348. """Replace an rdataset at name.
  349. It is not an error if there is no rdataset matching I{replacement}.
  350. Ownership of the *replacement* object is transferred to the zone;
  351. in other words, this method does not store a copy of *replacement*
  352. at the node, it stores *replacement* itself.
  353. If the node does not exist, it is created.
  354. *name*: the name of the node to find.
  355. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  356. name must be a subdomain of the zone's origin. If ``zone.relativize``
  357. is ``True``, then the name will be relativized.
  358. *replacement*, a ``dns.rdataset.Rdataset``, the replacement rdataset.
  359. """
  360. if replacement.rdclass != self.rdclass:
  361. raise ValueError("replacement.rdclass != zone.rdclass")
  362. node = self.find_node(name, True)
  363. node.replace_rdataset(replacement)
  364. def find_rrset(
  365. self,
  366. name: dns.name.Name | str,
  367. rdtype: dns.rdatatype.RdataType | str,
  368. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  369. ) -> dns.rrset.RRset:
  370. """Look for an rdataset with the specified name and type in the zone,
  371. and return an RRset encapsulating it.
  372. This method is less efficient than the similar
  373. ``find_rdataset()`` because it creates an RRset instead of
  374. returning the matching rdataset. It may be more convenient
  375. for some uses since it returns an object which binds the owner
  376. name to the rdataset.
  377. This method may not be used to create new nodes or rdatasets;
  378. use ``find_rdataset`` instead.
  379. *name*: the name of the node to find.
  380. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  381. name must be a subdomain of the zone's origin. If ``zone.relativize``
  382. is ``True``, then the name will be relativized.
  383. *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired.
  384. *covers*, a ``dns.rdatatype.RdataType`` or ``str``, the covered type.
  385. Usually this value is ``dns.rdatatype.NONE``, but if the
  386. rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
  387. then the covers value will be the rdata type the SIG/RRSIG
  388. covers. The library treats the SIG and RRSIG types as if they
  389. were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
  390. This makes RRSIGs much easier to work with than if RRSIGs
  391. covering different rdata types were aggregated into a single
  392. RRSIG rdataset.
  393. *create*, a ``bool``. If true, the node will be created if it does
  394. not exist.
  395. Raises ``KeyError`` if the name is not known and create was
  396. not specified, or if the name was not a subdomain of the origin.
  397. Returns a ``dns.rrset.RRset`` or ``None``.
  398. """
  399. vname = self._validate_name(name)
  400. rdtype = dns.rdatatype.RdataType.make(rdtype)
  401. covers = dns.rdatatype.RdataType.make(covers)
  402. rdataset = self.nodes[vname].find_rdataset(self.rdclass, rdtype, covers)
  403. rrset = dns.rrset.RRset(vname, self.rdclass, rdtype, covers)
  404. rrset.update(rdataset)
  405. return rrset
  406. def get_rrset(
  407. self,
  408. name: dns.name.Name | str,
  409. rdtype: dns.rdatatype.RdataType | str,
  410. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  411. ) -> dns.rrset.RRset | None:
  412. """Look for an rdataset with the specified name and type in the zone,
  413. and return an RRset encapsulating it.
  414. This method is less efficient than the similar ``get_rdataset()``
  415. because it creates an RRset instead of returning the matching
  416. rdataset. It may be more convenient for some uses since it
  417. returns an object which binds the owner name to the rdataset.
  418. This method may not be used to create new nodes or rdatasets;
  419. use ``get_rdataset()`` instead.
  420. *name*: the name of the node to find.
  421. The value may be a ``dns.name.Name`` or a ``str``. If absolute, the
  422. name must be a subdomain of the zone's origin. If ``zone.relativize``
  423. is ``True``, then the name will be relativized.
  424. *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired.
  425. *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type.
  426. Usually this value is ``dns.rdatatype.NONE``, but if the
  427. rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
  428. then the covers value will be the rdata type the SIG/RRSIG
  429. covers. The library treats the SIG and RRSIG types as if they
  430. were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
  431. This makes RRSIGs much easier to work with than if RRSIGs
  432. covering different rdata types were aggregated into a single
  433. RRSIG rdataset.
  434. *create*, a ``bool``. If true, the node will be created if it does
  435. not exist.
  436. Returns a ``dns.rrset.RRset`` or ``None``.
  437. """
  438. try:
  439. rrset = self.find_rrset(name, rdtype, covers)
  440. except KeyError:
  441. rrset = None
  442. return rrset
  443. def iterate_rdatasets(
  444. self,
  445. rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.ANY,
  446. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  447. ) -> Iterator[Tuple[dns.name.Name, dns.rdataset.Rdataset]]:
  448. """Return a generator which yields (name, rdataset) tuples for
  449. all rdatasets in the zone which have the specified *rdtype*
  450. and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default,
  451. then all rdatasets will be matched.
  452. *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired.
  453. *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type.
  454. Usually this value is ``dns.rdatatype.NONE``, but if the
  455. rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
  456. then the covers value will be the rdata type the SIG/RRSIG
  457. covers. The library treats the SIG and RRSIG types as if they
  458. were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
  459. This makes RRSIGs much easier to work with than if RRSIGs
  460. covering different rdata types were aggregated into a single
  461. RRSIG rdataset.
  462. """
  463. rdtype = dns.rdatatype.RdataType.make(rdtype)
  464. covers = dns.rdatatype.RdataType.make(covers)
  465. for name, node in self.items():
  466. for rds in node:
  467. if rdtype == dns.rdatatype.ANY or (
  468. rds.rdtype == rdtype and rds.covers == covers
  469. ):
  470. yield (name, rds)
  471. def iterate_rdatas(
  472. self,
  473. rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.ANY,
  474. covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE,
  475. ) -> Iterator[Tuple[dns.name.Name, int, dns.rdata.Rdata]]:
  476. """Return a generator which yields (name, ttl, rdata) tuples for
  477. all rdatas in the zone which have the specified *rdtype*
  478. and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default,
  479. then all rdatas will be matched.
  480. *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired.
  481. *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type.
  482. Usually this value is ``dns.rdatatype.NONE``, but if the
  483. rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
  484. then the covers value will be the rdata type the SIG/RRSIG
  485. covers. The library treats the SIG and RRSIG types as if they
  486. were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
  487. This makes RRSIGs much easier to work with than if RRSIGs
  488. covering different rdata types were aggregated into a single
  489. RRSIG rdataset.
  490. """
  491. rdtype = dns.rdatatype.RdataType.make(rdtype)
  492. covers = dns.rdatatype.RdataType.make(covers)
  493. for name, node in self.items():
  494. for rds in node:
  495. if rdtype == dns.rdatatype.ANY or (
  496. rds.rdtype == rdtype and rds.covers == covers
  497. ):
  498. for rdata in rds:
  499. yield (name, rds.ttl, rdata)
  500. def to_file(
  501. self,
  502. f: Any,
  503. sorted: bool = True,
  504. relativize: bool = True,
  505. nl: str | None = None,
  506. want_comments: bool = False,
  507. want_origin: bool = False,
  508. ) -> None:
  509. """Write a zone to a file.
  510. *f*, a file or `str`. If *f* is a string, it is treated
  511. as the name of a file to open.
  512. *sorted*, a ``bool``. If True, the default, then the file
  513. will be written with the names sorted in DNSSEC order from
  514. least to greatest. Otherwise the names will be written in
  515. whatever order they happen to have in the zone's dictionary.
  516. *relativize*, a ``bool``. If True, the default, then domain
  517. names in the output will be relativized to the zone's origin
  518. if possible.
  519. *nl*, a ``str`` or None. The end of line string. If not
  520. ``None``, the output will use the platform's native
  521. end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
  522. *want_comments*, a ``bool``. If ``True``, emit end-of-line comments
  523. as part of writing the file. If ``False``, the default, do not
  524. emit them.
  525. *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
  526. the start of the file. If ``False``, the default, do not emit
  527. one.
  528. """
  529. if isinstance(f, str):
  530. cm: contextlib.AbstractContextManager = open(f, "wb")
  531. else:
  532. cm = contextlib.nullcontext(f)
  533. with cm as f:
  534. # must be in this way, f.encoding may contain None, or even
  535. # attribute may not be there
  536. file_enc = getattr(f, "encoding", None)
  537. if file_enc is None:
  538. file_enc = "utf-8"
  539. if nl is None:
  540. # binary mode, '\n' is not enough
  541. nl_b = os.linesep.encode(file_enc)
  542. nl = "\n"
  543. elif isinstance(nl, str):
  544. nl_b = nl.encode(file_enc)
  545. else:
  546. nl_b = nl
  547. nl = nl.decode()
  548. if want_origin:
  549. assert self.origin is not None
  550. l = "$ORIGIN " + self.origin.to_text()
  551. l_b = l.encode(file_enc)
  552. try:
  553. f.write(l_b)
  554. f.write(nl_b)
  555. except TypeError: # textual mode
  556. f.write(l)
  557. f.write(nl)
  558. if sorted:
  559. names = list(self.keys())
  560. names.sort()
  561. else:
  562. names = self.keys()
  563. for n in names:
  564. l = self[n].to_text(
  565. n,
  566. origin=self.origin, # pyright: ignore
  567. relativize=relativize, # pyright: ignore
  568. want_comments=want_comments, # pyright: ignore
  569. )
  570. l_b = l.encode(file_enc)
  571. try:
  572. f.write(l_b)
  573. f.write(nl_b)
  574. except TypeError: # textual mode
  575. f.write(l)
  576. f.write(nl)
  577. def to_text(
  578. self,
  579. sorted: bool = True,
  580. relativize: bool = True,
  581. nl: str | None = None,
  582. want_comments: bool = False,
  583. want_origin: bool = False,
  584. ) -> str:
  585. """Return a zone's text as though it were written to a file.
  586. *sorted*, a ``bool``. If True, the default, then the file
  587. will be written with the names sorted in DNSSEC order from
  588. least to greatest. Otherwise the names will be written in
  589. whatever order they happen to have in the zone's dictionary.
  590. *relativize*, a ``bool``. If True, the default, then domain
  591. names in the output will be relativized to the zone's origin
  592. if possible.
  593. *nl*, a ``str`` or None. The end of line string. If not
  594. ``None``, the output will use the platform's native
  595. end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
  596. *want_comments*, a ``bool``. If ``True``, emit end-of-line comments
  597. as part of writing the file. If ``False``, the default, do not
  598. emit them.
  599. *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
  600. the start of the output. If ``False``, the default, do not emit
  601. one.
  602. Returns a ``str``.
  603. """
  604. temp_buffer = io.StringIO()
  605. self.to_file(temp_buffer, sorted, relativize, nl, want_comments, want_origin)
  606. return_value = temp_buffer.getvalue()
  607. temp_buffer.close()
  608. return return_value
  609. def check_origin(self) -> None:
  610. """Do some simple checking of the zone's origin.
  611. Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
  612. Raises ``dns.zone.NoNS`` if there is no NS RRset.
  613. Raises ``KeyError`` if there is no origin node.
  614. """
  615. if self.relativize:
  616. name = dns.name.empty
  617. else:
  618. assert self.origin is not None
  619. name = self.origin
  620. if self.get_rdataset(name, dns.rdatatype.SOA) is None:
  621. raise NoSOA
  622. if self.get_rdataset(name, dns.rdatatype.NS) is None:
  623. raise NoNS
  624. def get_soa(
  625. self, txn: dns.transaction.Transaction | None = None
  626. ) -> dns.rdtypes.ANY.SOA.SOA:
  627. """Get the zone SOA rdata.
  628. Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
  629. Returns a ``dns.rdtypes.ANY.SOA.SOA`` Rdata.
  630. """
  631. if self.relativize:
  632. origin_name = dns.name.empty
  633. else:
  634. if self.origin is None:
  635. # get_soa() has been called very early, and there must not be
  636. # an SOA if there is no origin.
  637. raise NoSOA
  638. origin_name = self.origin
  639. soa_rds: dns.rdataset.Rdataset | None
  640. if txn:
  641. soa_rds = txn.get(origin_name, dns.rdatatype.SOA)
  642. else:
  643. soa_rds = self.get_rdataset(origin_name, dns.rdatatype.SOA)
  644. if soa_rds is None:
  645. raise NoSOA
  646. else:
  647. soa = cast(dns.rdtypes.ANY.SOA.SOA, soa_rds[0])
  648. return soa
  649. def _compute_digest(
  650. self,
  651. hash_algorithm: DigestHashAlgorithm,
  652. scheme: DigestScheme = DigestScheme.SIMPLE,
  653. ) -> bytes:
  654. hashinfo = _digest_hashers.get(hash_algorithm)
  655. if not hashinfo:
  656. raise UnsupportedDigestHashAlgorithm
  657. if scheme != DigestScheme.SIMPLE:
  658. raise UnsupportedDigestScheme
  659. if self.relativize:
  660. origin_name = dns.name.empty
  661. else:
  662. assert self.origin is not None
  663. origin_name = self.origin
  664. hasher = hashinfo()
  665. for name, node in sorted(self.items()):
  666. rrnamebuf = name.to_digestable(self.origin)
  667. for rdataset in sorted(node, key=lambda rds: (rds.rdtype, rds.covers)):
  668. if name == origin_name and dns.rdatatype.ZONEMD in (
  669. rdataset.rdtype,
  670. rdataset.covers,
  671. ):
  672. continue
  673. rrfixed = struct.pack(
  674. "!HHI", rdataset.rdtype, rdataset.rdclass, rdataset.ttl
  675. )
  676. rdatas = [rdata.to_digestable(self.origin) for rdata in rdataset]
  677. for rdata in sorted(rdatas):
  678. rrlen = struct.pack("!H", len(rdata))
  679. hasher.update(rrnamebuf + rrfixed + rrlen + rdata)
  680. return hasher.digest()
  681. def compute_digest(
  682. self,
  683. hash_algorithm: DigestHashAlgorithm,
  684. scheme: DigestScheme = DigestScheme.SIMPLE,
  685. ) -> dns.rdtypes.ANY.ZONEMD.ZONEMD:
  686. serial = self.get_soa().serial
  687. digest = self._compute_digest(hash_algorithm, scheme)
  688. return dns.rdtypes.ANY.ZONEMD.ZONEMD(
  689. self.rdclass, dns.rdatatype.ZONEMD, serial, scheme, hash_algorithm, digest
  690. )
  691. def verify_digest(
  692. self, zonemd: dns.rdtypes.ANY.ZONEMD.ZONEMD | None = None
  693. ) -> None:
  694. digests: dns.rdataset.Rdataset | List[dns.rdtypes.ANY.ZONEMD.ZONEMD]
  695. if zonemd:
  696. digests = [zonemd]
  697. else:
  698. assert self.origin is not None
  699. rds = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD)
  700. if rds is None:
  701. raise NoDigest
  702. digests = rds
  703. for digest in digests:
  704. try:
  705. computed = self._compute_digest(digest.hash_algorithm, digest.scheme)
  706. if computed == digest.digest:
  707. return
  708. except Exception:
  709. pass
  710. raise DigestVerificationFailure
  711. # TransactionManager methods
  712. def reader(self) -> "Transaction":
  713. return Transaction(self, False, Version(self, 1, self.nodes, self.origin))
  714. def writer(self, replacement: bool = False) -> "Transaction":
  715. txn = Transaction(self, replacement)
  716. txn._setup_version()
  717. return txn
  718. def origin_information(
  719. self,
  720. ) -> Tuple[dns.name.Name | None, bool, dns.name.Name | None]:
  721. effective: dns.name.Name | None
  722. if self.relativize:
  723. effective = dns.name.empty
  724. else:
  725. effective = self.origin
  726. return (self.origin, self.relativize, effective)
  727. def get_class(self):
  728. return self.rdclass
  729. # Transaction methods
  730. def _end_read(self, txn):
  731. pass
  732. def _end_write(self, txn):
  733. pass
  734. def _commit_version(self, txn, version, origin):
  735. self.nodes = version.nodes
  736. if self.origin is None:
  737. self.origin = origin
  738. def _get_next_version_id(self) -> int:
  739. # Versions are ephemeral and all have id 1
  740. return 1
  741. # These classes used to be in dns.versioned, but have moved here so we can use
  742. # the copy-on-write transaction mechanism for both kinds of zones. In a
  743. # regular zone, the version only exists during the transaction, and the nodes
  744. # are regular dns.node.Nodes.
  745. # A node with a version id.
  746. class VersionedNode(dns.node.Node): # lgtm[py/missing-equals]
  747. __slots__ = ["id"]
  748. def __init__(self):
  749. super().__init__()
  750. # A proper id will get set by the Version
  751. self.id = 0
  752. @dns.immutable.immutable
  753. class ImmutableVersionedNode(VersionedNode):
  754. def __init__(self, node):
  755. super().__init__()
  756. self.id = node.id
  757. self.rdatasets = tuple(
  758. [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
  759. )
  760. def find_rdataset(
  761. self,
  762. rdclass: dns.rdataclass.RdataClass,
  763. rdtype: dns.rdatatype.RdataType,
  764. covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
  765. create: bool = False,
  766. ) -> dns.rdataset.Rdataset:
  767. if create:
  768. raise TypeError("immutable")
  769. return super().find_rdataset(rdclass, rdtype, covers, False)
  770. def get_rdataset(
  771. self,
  772. rdclass: dns.rdataclass.RdataClass,
  773. rdtype: dns.rdatatype.RdataType,
  774. covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
  775. create: bool = False,
  776. ) -> dns.rdataset.Rdataset | None:
  777. if create:
  778. raise TypeError("immutable")
  779. return super().get_rdataset(rdclass, rdtype, covers, False)
  780. def delete_rdataset(
  781. self,
  782. rdclass: dns.rdataclass.RdataClass,
  783. rdtype: dns.rdatatype.RdataType,
  784. covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
  785. ) -> None:
  786. raise TypeError("immutable")
  787. def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
  788. raise TypeError("immutable")
  789. def is_immutable(self) -> bool:
  790. return True
  791. class Version:
  792. def __init__(
  793. self,
  794. zone: Zone,
  795. id: int,
  796. nodes: MutableMapping[dns.name.Name, dns.node.Node] | None = None,
  797. origin: dns.name.Name | None = None,
  798. ):
  799. self.zone = zone
  800. self.id = id
  801. if nodes is not None:
  802. self.nodes = nodes
  803. else:
  804. self.nodes = zone.map_factory()
  805. self.origin = origin
  806. def _validate_name(self, name: dns.name.Name) -> dns.name.Name:
  807. return _validate_name(name, self.origin, self.zone.relativize)
  808. def get_node(self, name: dns.name.Name) -> dns.node.Node | None:
  809. name = self._validate_name(name)
  810. return self.nodes.get(name)
  811. def get_rdataset(
  812. self,
  813. name: dns.name.Name,
  814. rdtype: dns.rdatatype.RdataType,
  815. covers: dns.rdatatype.RdataType,
  816. ) -> dns.rdataset.Rdataset | None:
  817. node = self.get_node(name)
  818. if node is None:
  819. return None
  820. return node.get_rdataset(self.zone.rdclass, rdtype, covers)
  821. def keys(self):
  822. return self.nodes.keys()
  823. def items(self):
  824. return self.nodes.items()
  825. class WritableVersion(Version):
  826. def __init__(self, zone: Zone, replacement: bool = False):
  827. # The zone._versions_lock must be held by our caller in a versioned
  828. # zone.
  829. id = zone._get_next_version_id()
  830. super().__init__(zone, id)
  831. if not replacement:
  832. # We copy the map, because that gives us a simple and thread-safe
  833. # way of doing versions, and we have a garbage collector to help
  834. # us. We only make new node objects if we actually change the
  835. # node.
  836. self.nodes.update(zone.nodes)
  837. # We have to copy the zone origin as it may be None in the first
  838. # version, and we don't want to mutate the zone until we commit.
  839. self.origin = zone.origin
  840. self.changed: Set[dns.name.Name] = set()
  841. def _maybe_cow_with_name(
  842. self, name: dns.name.Name
  843. ) -> Tuple[dns.node.Node, dns.name.Name]:
  844. name = self._validate_name(name)
  845. node = self.nodes.get(name)
  846. if node is None or name not in self.changed:
  847. new_node = self.zone.node_factory()
  848. if hasattr(new_node, "id"):
  849. # We keep doing this for backwards compatibility, as earlier
  850. # code used new_node.id != self.id for the "do we need to CoW?"
  851. # test. Now we use the changed set as this works with both
  852. # regular zones and versioned zones.
  853. #
  854. # We ignore the mypy error as this is safe but it doesn't see it.
  855. new_node.id = self.id # type: ignore
  856. if node is not None:
  857. # moo! copy on write!
  858. new_node.rdatasets.extend(node.rdatasets)
  859. self.nodes[name] = new_node
  860. self.changed.add(name)
  861. return (new_node, name)
  862. else:
  863. return (node, name)
  864. def _maybe_cow(self, name: dns.name.Name) -> dns.node.Node:
  865. return self._maybe_cow_with_name(name)[0]
  866. def delete_node(self, name: dns.name.Name) -> None:
  867. name = self._validate_name(name)
  868. if name in self.nodes:
  869. del self.nodes[name]
  870. self.changed.add(name)
  871. def put_rdataset(
  872. self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset
  873. ) -> None:
  874. node = self._maybe_cow(name)
  875. node.replace_rdataset(rdataset)
  876. def delete_rdataset(
  877. self,
  878. name: dns.name.Name,
  879. rdtype: dns.rdatatype.RdataType,
  880. covers: dns.rdatatype.RdataType,
  881. ) -> None:
  882. node = self._maybe_cow(name)
  883. node.delete_rdataset(self.zone.rdclass, rdtype, covers)
  884. if len(node) == 0:
  885. del self.nodes[name]
  886. @dns.immutable.immutable
  887. class ImmutableVersion(Version):
  888. def __init__(self, version: Version):
  889. if not isinstance(version, WritableVersion):
  890. raise ValueError(
  891. "a dns.zone.ImmutableVersion requires a dns.zone.WritableVersion"
  892. )
  893. # We tell super() that it's a replacement as we don't want it
  894. # to copy the nodes, as we're about to do that with an
  895. # immutable Dict.
  896. super().__init__(version.zone, True)
  897. # set the right id!
  898. self.id = version.id
  899. # keep the origin
  900. self.origin = version.origin
  901. # Make changed nodes immutable
  902. for name in version.changed:
  903. node = version.nodes.get(name)
  904. # it might not exist if we deleted it in the version
  905. if node:
  906. version.nodes[name] = ImmutableVersionedNode(node)
  907. # We're changing the type of the nodes dictionary here on purpose, so
  908. # we ignore the mypy error.
  909. self.nodes = dns.immutable.Dict(
  910. version.nodes, True, self.zone.map_factory
  911. ) # type: ignore
  912. class Transaction(dns.transaction.Transaction):
  913. def __init__(self, zone, replacement, version=None, make_immutable=False):
  914. read_only = version is not None
  915. super().__init__(zone, replacement, read_only)
  916. self.version = version
  917. self.make_immutable = make_immutable
  918. @property
  919. def zone(self):
  920. return self.manager
  921. def _setup_version(self):
  922. assert self.version is None
  923. factory = self.manager.writable_version_factory # pyright: ignore
  924. if factory is None:
  925. factory = WritableVersion
  926. self.version = factory(self.zone, self.replacement) # pyright: ignore
  927. def _get_rdataset(self, name, rdtype, covers):
  928. assert self.version is not None
  929. return self.version.get_rdataset(name, rdtype, covers)
  930. def _put_rdataset(self, name, rdataset):
  931. assert not self.read_only
  932. assert self.version is not None
  933. self.version.put_rdataset(name, rdataset)
  934. def _delete_name(self, name):
  935. assert not self.read_only
  936. assert self.version is not None
  937. self.version.delete_node(name)
  938. def _delete_rdataset(self, name, rdtype, covers):
  939. assert not self.read_only
  940. assert self.version is not None
  941. self.version.delete_rdataset(name, rdtype, covers)
  942. def _name_exists(self, name):
  943. assert self.version is not None
  944. return self.version.get_node(name) is not None
  945. def _changed(self):
  946. if self.read_only:
  947. return False
  948. else:
  949. assert self.version is not None
  950. return len(self.version.changed) > 0
  951. def _end_transaction(self, commit):
  952. assert self.zone is not None
  953. assert self.version is not None
  954. if self.read_only:
  955. self.zone._end_read(self) # pyright: ignore
  956. elif commit and len(self.version.changed) > 0:
  957. if self.make_immutable:
  958. factory = self.manager.immutable_version_factory # pyright: ignore
  959. if factory is None:
  960. factory = ImmutableVersion
  961. version = factory(self.version)
  962. else:
  963. version = self.version
  964. self.zone._commit_version( # pyright: ignore
  965. self, version, self.version.origin
  966. )
  967. else:
  968. # rollback
  969. self.zone._end_write(self) # pyright: ignore
  970. def _set_origin(self, origin):
  971. assert self.version is not None
  972. if self.version.origin is None:
  973. self.version.origin = origin
  974. def _iterate_rdatasets(self):
  975. assert self.version is not None
  976. for name, node in self.version.items():
  977. for rdataset in node:
  978. yield (name, rdataset)
  979. def _iterate_names(self):
  980. assert self.version is not None
  981. return self.version.keys()
  982. def _get_node(self, name):
  983. assert self.version is not None
  984. return self.version.get_node(name)
  985. def _origin_information(self):
  986. assert self.version is not None
  987. (absolute, relativize, effective) = self.manager.origin_information()
  988. if absolute is None and self.version.origin is not None:
  989. # No origin has been committed yet, but we've learned one as part of
  990. # this txn. Use it.
  991. absolute = self.version.origin
  992. if relativize:
  993. effective = dns.name.empty
  994. else:
  995. effective = absolute
  996. return (absolute, relativize, effective)
  997. def _from_text(
  998. text: Any,
  999. origin: dns.name.Name | str | None = None,
  1000. rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
  1001. relativize: bool = True,
  1002. zone_factory: Any = Zone,
  1003. filename: str | None = None,
  1004. allow_include: bool = False,
  1005. check_origin: bool = True,
  1006. idna_codec: dns.name.IDNACodec | None = None,
  1007. allow_directives: bool | Iterable[str] = True,
  1008. ) -> Zone:
  1009. # See the comments for the public APIs from_text() and from_file() for
  1010. # details.
  1011. # 'text' can also be a file, but we don't publish that fact
  1012. # since it's an implementation detail. The official file
  1013. # interface is from_file().
  1014. if filename is None:
  1015. filename = "<string>"
  1016. zone = zone_factory(origin, rdclass, relativize=relativize)
  1017. with zone.writer(True) as txn:
  1018. tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
  1019. reader = dns.zonefile.Reader(
  1020. tok,
  1021. rdclass,
  1022. txn,
  1023. allow_include=allow_include,
  1024. allow_directives=allow_directives,
  1025. )
  1026. try:
  1027. reader.read()
  1028. except dns.zonefile.UnknownOrigin:
  1029. # for backwards compatibility
  1030. raise UnknownOrigin
  1031. # Now that we're done reading, do some basic checking of the zone.
  1032. if check_origin:
  1033. zone.check_origin()
  1034. return zone
  1035. def from_text(
  1036. text: str,
  1037. origin: dns.name.Name | str | None = None,
  1038. rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
  1039. relativize: bool = True,
  1040. zone_factory: Any = Zone,
  1041. filename: str | None = None,
  1042. allow_include: bool = False,
  1043. check_origin: bool = True,
  1044. idna_codec: dns.name.IDNACodec | None = None,
  1045. allow_directives: bool | Iterable[str] = True,
  1046. ) -> Zone:
  1047. """Build a zone object from a zone file format string.
  1048. *text*, a ``str``, the zone file format input.
  1049. *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
  1050. of the zone; if not specified, the first ``$ORIGIN`` statement in the
  1051. zone file will determine the origin of the zone.
  1052. *rdclass*, a ``dns.rdataclass.RdataClass``, the zone's rdata class; the default is
  1053. class IN.
  1054. *relativize*, a ``bool``, determine's whether domain names are
  1055. relativized to the zone's origin. The default is ``True``.
  1056. *zone_factory*, the zone factory to use or ``None``. If ``None``, then
  1057. ``dns.zone.Zone`` will be used. The value may be any class or callable
  1058. that returns a subclass of ``dns.zone.Zone``.
  1059. *filename*, a ``str`` or ``None``, the filename to emit when
  1060. describing where an error occurred; the default is ``'<string>'``.
  1061. *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE``
  1062. directives are permitted. If ``False``, then encoutering a ``$INCLUDE``
  1063. will raise a ``SyntaxError`` exception.
  1064. *check_origin*, a ``bool``. If ``True``, the default, then sanity
  1065. checks of the origin node will be made by calling the zone's
  1066. ``check_origin()`` method.
  1067. *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
  1068. encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
  1069. is used.
  1070. *allow_directives*, a ``bool`` or an iterable of `str`. If ``True``, the default,
  1071. then directives are permitted, and the *allow_include* parameter controls whether
  1072. ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive
  1073. processing is done and any directive-like text will be treated as a regular owner
  1074. name. If a non-empty iterable, then only the listed directives (including the
  1075. ``$``) are allowed.
  1076. Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
  1077. Raises ``dns.zone.NoNS`` if there is no NS RRset.
  1078. Raises ``KeyError`` if there is no origin node.
  1079. Returns a subclass of ``dns.zone.Zone``.
  1080. """
  1081. return _from_text(
  1082. text,
  1083. origin,
  1084. rdclass,
  1085. relativize,
  1086. zone_factory,
  1087. filename,
  1088. allow_include,
  1089. check_origin,
  1090. idna_codec,
  1091. allow_directives,
  1092. )
  1093. def from_file(
  1094. f: Any,
  1095. origin: dns.name.Name | str | None = None,
  1096. rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN,
  1097. relativize: bool = True,
  1098. zone_factory: Any = Zone,
  1099. filename: str | None = None,
  1100. allow_include: bool = True,
  1101. check_origin: bool = True,
  1102. idna_codec: dns.name.IDNACodec | None = None,
  1103. allow_directives: bool | Iterable[str] = True,
  1104. ) -> Zone:
  1105. """Read a zone file and build a zone object.
  1106. *f*, a file or ``str``. If *f* is a string, it is treated
  1107. as the name of a file to open.
  1108. *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
  1109. of the zone; if not specified, the first ``$ORIGIN`` statement in the
  1110. zone file will determine the origin of the zone.
  1111. *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
  1112. *relativize*, a ``bool``, determine's whether domain names are
  1113. relativized to the zone's origin. The default is ``True``.
  1114. *zone_factory*, the zone factory to use or ``None``. If ``None``, then
  1115. ``dns.zone.Zone`` will be used. The value may be any class or callable
  1116. that returns a subclass of ``dns.zone.Zone``.
  1117. *filename*, a ``str`` or ``None``, the filename to emit when
  1118. describing where an error occurred; the default is ``'<string>'``.
  1119. *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE``
  1120. directives are permitted. If ``False``, then encoutering a ``$INCLUDE``
  1121. will raise a ``SyntaxError`` exception.
  1122. *check_origin*, a ``bool``. If ``True``, the default, then sanity
  1123. checks of the origin node will be made by calling the zone's
  1124. ``check_origin()`` method.
  1125. *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
  1126. encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
  1127. is used.
  1128. *allow_directives*, a ``bool`` or an iterable of `str`. If ``True``, the default,
  1129. then directives are permitted, and the *allow_include* parameter controls whether
  1130. ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive
  1131. processing is done and any directive-like text will be treated as a regular owner
  1132. name. If a non-empty iterable, then only the listed directives (including the
  1133. ``$``) are allowed.
  1134. Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
  1135. Raises ``dns.zone.NoNS`` if there is no NS RRset.
  1136. Raises ``KeyError`` if there is no origin node.
  1137. Returns a subclass of ``dns.zone.Zone``.
  1138. """
  1139. if isinstance(f, str):
  1140. if filename is None:
  1141. filename = f
  1142. cm: contextlib.AbstractContextManager = open(f, encoding="utf-8")
  1143. else:
  1144. cm = contextlib.nullcontext(f)
  1145. with cm as f:
  1146. return _from_text(
  1147. f,
  1148. origin,
  1149. rdclass,
  1150. relativize,
  1151. zone_factory,
  1152. filename,
  1153. allow_include,
  1154. check_origin,
  1155. idna_codec,
  1156. allow_directives,
  1157. )
  1158. assert False # make mypy happy lgtm[py/unreachable-statement]
  1159. def from_xfr(
  1160. xfr: Any,
  1161. zone_factory: Any = Zone,
  1162. relativize: bool = True,
  1163. check_origin: bool = True,
  1164. ) -> Zone:
  1165. """Convert the output of a zone transfer generator into a zone object.
  1166. *xfr*, a generator of ``dns.message.Message`` objects, typically
  1167. ``dns.query.xfr()``.
  1168. *relativize*, a ``bool``, determine's whether domain names are
  1169. relativized to the zone's origin. The default is ``True``.
  1170. It is essential that the relativize setting matches the one specified
  1171. to the generator.
  1172. *check_origin*, a ``bool``. If ``True``, the default, then sanity
  1173. checks of the origin node will be made by calling the zone's
  1174. ``check_origin()`` method.
  1175. Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
  1176. Raises ``dns.zone.NoNS`` if there is no NS RRset.
  1177. Raises ``KeyError`` if there is no origin node.
  1178. Raises ``ValueError`` if no messages are yielded by the generator.
  1179. Returns a subclass of ``dns.zone.Zone``.
  1180. """
  1181. z = None
  1182. for r in xfr:
  1183. if z is None:
  1184. if relativize:
  1185. origin = r.origin
  1186. else:
  1187. origin = r.answer[0].name
  1188. rdclass = r.answer[0].rdclass
  1189. z = zone_factory(origin, rdclass, relativize=relativize)
  1190. for rrset in r.answer:
  1191. znode = z.nodes.get(rrset.name)
  1192. if not znode:
  1193. znode = z.node_factory()
  1194. z.nodes[rrset.name] = znode
  1195. zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype, rrset.covers, True)
  1196. zrds.update_ttl(rrset.ttl)
  1197. for rd in rrset:
  1198. zrds.add(rd)
  1199. if z is None:
  1200. raise ValueError("empty transfer")
  1201. if check_origin:
  1202. z.check_origin()
  1203. return z