rdata.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. # Copyright (C) 2001-2007, 2009-2011 Nominum, Inc.
  2. #
  3. # Permission to use, copy, modify, and distribute this software and its
  4. # documentation for any purpose with or without fee is hereby granted,
  5. # provided that the above copyright notice and this permission notice
  6. # appear in all copies.
  7. #
  8. # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
  9. # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
  11. # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  14. # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. """DNS rdata.
  16. @var _rdata_modules: A dictionary mapping a (rdclass, rdtype) tuple to
  17. the module which implements that type.
  18. @type _rdata_modules: dict
  19. @var _module_prefix: The prefix to use when forming modules names. The
  20. default is 'dns.rdtypes'. Changing this value will break the library.
  21. @type _module_prefix: string
  22. @var _hex_chunk: At most this many octets that will be represented in each
  23. chunk of hexstring that _hexify() produces before whitespace occurs.
  24. @type _hex_chunk: int"""
  25. from io import BytesIO
  26. import base64
  27. import binascii
  28. import dns.exception
  29. import dns.name
  30. import dns.rdataclass
  31. import dns.rdatatype
  32. import dns.tokenizer
  33. import dns.wiredata
  34. from ._compat import xrange, string_types, text_type
  35. _hex_chunksize = 32
  36. def _hexify(data, chunksize=_hex_chunksize):
  37. """Convert a binary string into its hex encoding, broken up into chunks
  38. of I{chunksize} characters separated by a space.
  39. @param data: the binary string
  40. @type data: string
  41. @param chunksize: the chunk size. Default is L{dns.rdata._hex_chunksize}
  42. @rtype: string
  43. """
  44. line = binascii.hexlify(data)
  45. return b' '.join([line[i:i + chunksize]
  46. for i
  47. in range(0, len(line), chunksize)]).decode()
  48. _base64_chunksize = 32
  49. def _base64ify(data, chunksize=_base64_chunksize):
  50. """Convert a binary string into its base64 encoding, broken up into chunks
  51. of I{chunksize} characters separated by a space.
  52. @param data: the binary string
  53. @type data: string
  54. @param chunksize: the chunk size. Default is
  55. L{dns.rdata._base64_chunksize}
  56. @rtype: string
  57. """
  58. line = base64.b64encode(data)
  59. return b' '.join([line[i:i + chunksize]
  60. for i
  61. in range(0, len(line), chunksize)]).decode()
  62. __escaped = bytearray(b'"\\')
  63. def _escapify(qstring):
  64. """Escape the characters in a quoted string which need it.
  65. @param qstring: the string
  66. @type qstring: string
  67. @returns: the escaped string
  68. @rtype: string
  69. """
  70. if isinstance(qstring, text_type):
  71. qstring = qstring.encode()
  72. if not isinstance(qstring, bytearray):
  73. qstring = bytearray(qstring)
  74. text = ''
  75. for c in qstring:
  76. if c in __escaped:
  77. text += '\\' + chr(c)
  78. elif c >= 0x20 and c < 0x7F:
  79. text += chr(c)
  80. else:
  81. text += '\\%03d' % c
  82. return text
  83. def _truncate_bitmap(what):
  84. """Determine the index of greatest byte that isn't all zeros, and
  85. return the bitmap that contains all the bytes less than that index.
  86. @param what: a string of octets representing a bitmap.
  87. @type what: string
  88. @rtype: string
  89. """
  90. for i in xrange(len(what) - 1, -1, -1):
  91. if what[i] != 0:
  92. return what[0: i + 1]
  93. return what[0:1]
  94. class Rdata(object):
  95. """Base class for all DNS rdata types.
  96. """
  97. __slots__ = ['rdclass', 'rdtype']
  98. def __init__(self, rdclass, rdtype):
  99. """Initialize an rdata.
  100. @param rdclass: The rdata class
  101. @type rdclass: int
  102. @param rdtype: The rdata type
  103. @type rdtype: int
  104. """
  105. self.rdclass = rdclass
  106. self.rdtype = rdtype
  107. def covers(self):
  108. """DNS SIG/RRSIG rdatas apply to a specific type; this type is
  109. returned by the covers() function. If the rdata type is not
  110. SIG or RRSIG, dns.rdatatype.NONE is returned. This is useful when
  111. creating rdatasets, allowing the rdataset to contain only RRSIGs
  112. of a particular type, e.g. RRSIG(NS).
  113. @rtype: int
  114. """
  115. return dns.rdatatype.NONE
  116. def extended_rdatatype(self):
  117. """Return a 32-bit type value, the least significant 16 bits of
  118. which are the ordinary DNS type, and the upper 16 bits of which are
  119. the "covered" type, if any.
  120. @rtype: int
  121. """
  122. return self.covers() << 16 | self.rdtype
  123. def to_text(self, origin=None, relativize=True, **kw):
  124. """Convert an rdata to text format.
  125. @rtype: string
  126. """
  127. raise NotImplementedError
  128. def to_wire(self, file, compress=None, origin=None):
  129. """Convert an rdata to wire format.
  130. @rtype: string
  131. """
  132. raise NotImplementedError
  133. def to_digestable(self, origin=None):
  134. """Convert rdata to a format suitable for digesting in hashes. This
  135. is also the DNSSEC canonical form."""
  136. f = BytesIO()
  137. self.to_wire(f, None, origin)
  138. return f.getvalue()
  139. def validate(self):
  140. """Check that the current contents of the rdata's fields are
  141. valid. If you change an rdata by assigning to its fields,
  142. it is a good idea to call validate() when you are done making
  143. changes.
  144. """
  145. dns.rdata.from_text(self.rdclass, self.rdtype, self.to_text())
  146. def __repr__(self):
  147. covers = self.covers()
  148. if covers == dns.rdatatype.NONE:
  149. ctext = ''
  150. else:
  151. ctext = '(' + dns.rdatatype.to_text(covers) + ')'
  152. return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
  153. dns.rdatatype.to_text(self.rdtype) + ctext + ' rdata: ' + \
  154. str(self) + '>'
  155. def __str__(self):
  156. return self.to_text()
  157. def _cmp(self, other):
  158. """Compare an rdata with another rdata of the same rdtype and
  159. rdclass. Return < 0 if self < other in the DNSSEC ordering,
  160. 0 if self == other, and > 0 if self > other.
  161. """
  162. our = self.to_digestable(dns.name.root)
  163. their = other.to_digestable(dns.name.root)
  164. if our == their:
  165. return 0
  166. if our > their:
  167. return 1
  168. return -1
  169. def __eq__(self, other):
  170. if not isinstance(other, Rdata):
  171. return False
  172. if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
  173. return False
  174. return self._cmp(other) == 0
  175. def __ne__(self, other):
  176. if not isinstance(other, Rdata):
  177. return True
  178. if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
  179. return True
  180. return self._cmp(other) != 0
  181. def __lt__(self, other):
  182. if not isinstance(other, Rdata) or \
  183. self.rdclass != other.rdclass or self.rdtype != other.rdtype:
  184. return NotImplemented
  185. return self._cmp(other) < 0
  186. def __le__(self, other):
  187. if not isinstance(other, Rdata) or \
  188. self.rdclass != other.rdclass or self.rdtype != other.rdtype:
  189. return NotImplemented
  190. return self._cmp(other) <= 0
  191. def __ge__(self, other):
  192. if not isinstance(other, Rdata) or \
  193. self.rdclass != other.rdclass or self.rdtype != other.rdtype:
  194. return NotImplemented
  195. return self._cmp(other) >= 0
  196. def __gt__(self, other):
  197. if not isinstance(other, Rdata) or \
  198. self.rdclass != other.rdclass or self.rdtype != other.rdtype:
  199. return NotImplemented
  200. return self._cmp(other) > 0
  201. def __hash__(self):
  202. return hash(self.to_digestable(dns.name.root))
  203. @classmethod
  204. def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
  205. """Build an rdata object from text format.
  206. @param rdclass: The rdata class
  207. @type rdclass: int
  208. @param rdtype: The rdata type
  209. @type rdtype: int
  210. @param tok: The tokenizer
  211. @type tok: dns.tokenizer.Tokenizer
  212. @param origin: The origin to use for relative names
  213. @type origin: dns.name.Name
  214. @param relativize: should names be relativized?
  215. @type relativize: bool
  216. @rtype: dns.rdata.Rdata instance
  217. """
  218. raise NotImplementedError
  219. @classmethod
  220. def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
  221. """Build an rdata object from wire format
  222. @param rdclass: The rdata class
  223. @type rdclass: int
  224. @param rdtype: The rdata type
  225. @type rdtype: int
  226. @param wire: The wire-format message
  227. @type wire: string
  228. @param current: The offset in wire of the beginning of the rdata.
  229. @type current: int
  230. @param rdlen: The length of the wire-format rdata
  231. @type rdlen: int
  232. @param origin: The origin to use for relative names
  233. @type origin: dns.name.Name
  234. @rtype: dns.rdata.Rdata instance
  235. """
  236. raise NotImplementedError
  237. def choose_relativity(self, origin=None, relativize=True):
  238. """Convert any domain names in the rdata to the specified
  239. relativization.
  240. """
  241. pass
  242. class GenericRdata(Rdata):
  243. """Generate Rdata Class
  244. This class is used for rdata types for which we have no better
  245. implementation. It implements the DNS "unknown RRs" scheme.
  246. """
  247. __slots__ = ['data']
  248. def __init__(self, rdclass, rdtype, data):
  249. super(GenericRdata, self).__init__(rdclass, rdtype)
  250. self.data = data
  251. def to_text(self, origin=None, relativize=True, **kw):
  252. return r'\# %d ' % len(self.data) + _hexify(self.data)
  253. @classmethod
  254. def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
  255. token = tok.get()
  256. if not token.is_identifier() or token.value != '\#':
  257. raise dns.exception.SyntaxError(
  258. r'generic rdata does not start with \#')
  259. length = tok.get_int()
  260. chunks = []
  261. while 1:
  262. token = tok.get()
  263. if token.is_eol_or_eof():
  264. break
  265. chunks.append(token.value.encode())
  266. hex = b''.join(chunks)
  267. data = binascii.unhexlify(hex)
  268. if len(data) != length:
  269. raise dns.exception.SyntaxError(
  270. 'generic rdata hex data has wrong length')
  271. return cls(rdclass, rdtype, data)
  272. def to_wire(self, file, compress=None, origin=None):
  273. file.write(self.data)
  274. @classmethod
  275. def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
  276. return cls(rdclass, rdtype, wire[current: current + rdlen])
  277. _rdata_modules = {}
  278. _module_prefix = 'dns.rdtypes'
  279. def get_rdata_class(rdclass, rdtype):
  280. def import_module(name):
  281. mod = __import__(name)
  282. components = name.split('.')
  283. for comp in components[1:]:
  284. mod = getattr(mod, comp)
  285. return mod
  286. mod = _rdata_modules.get((rdclass, rdtype))
  287. rdclass_text = dns.rdataclass.to_text(rdclass)
  288. rdtype_text = dns.rdatatype.to_text(rdtype)
  289. rdtype_text = rdtype_text.replace('-', '_')
  290. if not mod:
  291. mod = _rdata_modules.get((dns.rdatatype.ANY, rdtype))
  292. if not mod:
  293. try:
  294. mod = import_module('.'.join([_module_prefix,
  295. rdclass_text, rdtype_text]))
  296. _rdata_modules[(rdclass, rdtype)] = mod
  297. except ImportError:
  298. try:
  299. mod = import_module('.'.join([_module_prefix,
  300. 'ANY', rdtype_text]))
  301. _rdata_modules[(dns.rdataclass.ANY, rdtype)] = mod
  302. except ImportError:
  303. mod = None
  304. if mod:
  305. cls = getattr(mod, rdtype_text)
  306. else:
  307. cls = GenericRdata
  308. return cls
  309. def from_text(rdclass, rdtype, tok, origin=None, relativize=True):
  310. """Build an rdata object from text format.
  311. This function attempts to dynamically load a class which
  312. implements the specified rdata class and type. If there is no
  313. class-and-type-specific implementation, the GenericRdata class
  314. is used.
  315. Once a class is chosen, its from_text() class method is called
  316. with the parameters to this function.
  317. If I{tok} is a string, then a tokenizer is created and the string
  318. is used as its input.
  319. @param rdclass: The rdata class
  320. @type rdclass: int
  321. @param rdtype: The rdata type
  322. @type rdtype: int
  323. @param tok: The tokenizer or input text
  324. @type tok: dns.tokenizer.Tokenizer or string
  325. @param origin: The origin to use for relative names
  326. @type origin: dns.name.Name
  327. @param relativize: Should names be relativized?
  328. @type relativize: bool
  329. @rtype: dns.rdata.Rdata instance"""
  330. if isinstance(tok, string_types):
  331. tok = dns.tokenizer.Tokenizer(tok)
  332. cls = get_rdata_class(rdclass, rdtype)
  333. if cls != GenericRdata:
  334. # peek at first token
  335. token = tok.get()
  336. tok.unget(token)
  337. if token.is_identifier() and \
  338. token.value == r'\#':
  339. #
  340. # Known type using the generic syntax. Extract the
  341. # wire form from the generic syntax, and then run
  342. # from_wire on it.
  343. #
  344. rdata = GenericRdata.from_text(rdclass, rdtype, tok, origin,
  345. relativize)
  346. return from_wire(rdclass, rdtype, rdata.data, 0, len(rdata.data),
  347. origin)
  348. return cls.from_text(rdclass, rdtype, tok, origin, relativize)
  349. def from_wire(rdclass, rdtype, wire, current, rdlen, origin=None):
  350. """Build an rdata object from wire format
  351. This function attempts to dynamically load a class which
  352. implements the specified rdata class and type. If there is no
  353. class-and-type-specific implementation, the GenericRdata class
  354. is used.
  355. Once a class is chosen, its from_wire() class method is called
  356. with the parameters to this function.
  357. @param rdclass: The rdata class
  358. @type rdclass: int
  359. @param rdtype: The rdata type
  360. @type rdtype: int
  361. @param wire: The wire-format message
  362. @type wire: string
  363. @param current: The offset in wire of the beginning of the rdata.
  364. @type current: int
  365. @param rdlen: The length of the wire-format rdata
  366. @type rdlen: int
  367. @param origin: The origin to use for relative names
  368. @type origin: dns.name.Name
  369. @rtype: dns.rdata.Rdata instance"""
  370. wire = dns.wiredata.maybe_wrap(wire)
  371. cls = get_rdata_class(rdclass, rdtype)
  372. return cls.from_wire(rdclass, rdtype, wire, current, rdlen, origin)