Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dns/renderer.py: 28%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
3# Copyright (C) 2001-2017 Nominum, Inc.
4#
5# Permission to use, copy, modify, and distribute this software and its
6# documentation for any purpose with or without fee is hereby granted,
7# provided that the above copyright notice and this permission notice
8# appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18"""Help for building DNS wire format messages"""
20import contextlib
21import io
22import random
23import struct
24import time
26import dns.edns
27import dns.exception
28import dns.name
29import dns.rdataclass
30import dns.rdatatype
31import dns.rdtypes.ANY.OPT
32import dns.rdtypes.ANY.TSIG
33import dns.rrset
34import dns.tsig
35from dns._render_util import prefixed_length as prefixed_length # pyright: ignore
37DEFAULT_EDNS_PAYLOAD = 1232
39QUESTION = 0
40ANSWER = 1
41AUTHORITY = 2
42ADDITIONAL = 3
45def _make_opt(flags=0, payload=DEFAULT_EDNS_PAYLOAD, options=None):
46 opt = dns.rdtypes.ANY.OPT.OPT(payload, dns.rdatatype.OPT, options or ())
47 return dns.rrset.from_rdata(dns.name.root, int(flags), opt)
50def _make_tsig(keyname, algorithm, time_signed, fudge, mac, original_id, error, other):
51 tsig = dns.rdtypes.ANY.TSIG.TSIG(
52 dns.rdataclass.ANY,
53 dns.rdatatype.TSIG,
54 algorithm,
55 time_signed,
56 fudge,
57 mac,
58 original_id,
59 error,
60 other,
61 )
62 return dns.rrset.from_rdata(keyname, 0, tsig)
65class Renderer:
66 """Helper class for building DNS wire-format messages.
68 Most applications can use the higher-level L{dns.message.Message}
69 class and its to_wire() method to generate wire-format messages.
70 This class is for those applications which need finer control
71 over the generation of messages.
73 Typical use::
75 r = dns.renderer.Renderer(id=1, flags=0x80, max_size=512)
76 r.add_question(qname, qtype, qclass)
77 r.add_rrset(dns.renderer.ANSWER, rrset_1)
78 r.add_rrset(dns.renderer.ANSWER, rrset_2)
79 r.add_rrset(dns.renderer.AUTHORITY, ns_rrset)
80 r.add_rrset(dns.renderer.ADDITIONAL, ad_rrset_1)
81 r.add_rrset(dns.renderer.ADDITIONAL, ad_rrset_2)
82 r.add_edns(0, 0, 4096)
83 r.write_header()
84 r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac)
85 wire = r.get_wire()
87 If padding is going to be used, then the OPT record MUST be
88 written after everything else in the additional section except for
89 the TSIG (if any).
91 output, an io.BytesIO, where rendering is written
93 id: the message id
95 flags: the message flags
97 max_size: the maximum size of the message
99 origin: the origin to use when rendering relative names
101 compress: the compression table
103 section: an int, the section currently being rendered
105 counts: list of the number of RRs in each section
107 mac: the MAC of the rendered message (if TSIG was used)
108 """
110 def __init__(self, id=None, flags=0, max_size=65535, origin=None):
111 """Initialize a new renderer."""
113 self.output = io.BytesIO()
114 if id is None:
115 self.id = random.randint(0, 65535)
116 else:
117 self.id = id
118 self.flags = flags
119 self.max_size = max_size
120 self.origin = origin
121 self.compress = {}
122 self.section = QUESTION
123 self.counts = [0, 0, 0, 0]
124 self.output.write(b"\x00" * 12)
125 self.mac = ""
126 self.reserved = 0
127 self.was_padded = False
129 def _rollback(self, where):
130 """Truncate the output buffer at offset *where*, and remove any
131 compression table entries that pointed beyond the truncation
132 point.
133 """
135 self.output.seek(where)
136 self.output.truncate()
137 keys_to_delete = []
138 for k, v in self.compress.items():
139 if v >= where:
140 keys_to_delete.append(k)
141 for k in keys_to_delete:
142 del self.compress[k]
144 def _set_section(self, section):
145 """Set the renderer's current section.
147 Sections must be rendered order: QUESTION, ANSWER, AUTHORITY,
148 ADDITIONAL. Sections may be empty.
150 Raises dns.exception.FormError if an attempt was made to set
151 a section value less than the current section.
152 """
154 if self.section != section:
155 if self.section > section:
156 raise dns.exception.FormError
157 self.section = section
159 @contextlib.contextmanager
160 def _track_size(self):
161 start = self.output.tell()
162 yield start
163 if self.output.tell() > self.max_size:
164 self._rollback(start)
165 raise dns.exception.TooBig
167 @contextlib.contextmanager
168 def _temporarily_seek_to(self, where):
169 current = self.output.tell()
170 try:
171 self.output.seek(where)
172 yield
173 finally:
174 self.output.seek(current)
176 def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN):
177 """Add a question to the message."""
179 self._set_section(QUESTION)
180 with self._track_size():
181 qname.to_wire(self.output, self.compress, self.origin)
182 self.output.write(struct.pack("!HH", rdtype, rdclass))
183 self.counts[QUESTION] += 1
185 def add_rrset(self, section, rrset, **kw):
186 """Add the rrset to the specified section.
188 Any keyword arguments are passed on to the rdataset's to_wire()
189 routine.
190 """
192 self._set_section(section)
193 with self._track_size():
194 n = rrset.to_wire(self.output, self.compress, self.origin, **kw)
195 self.counts[section] += n
197 def add_rdataset(self, section, name, rdataset, **kw):
198 """Add the rdataset to the specified section, using the specified
199 name as the owner name.
201 Any keyword arguments are passed on to the rdataset's to_wire()
202 routine.
203 """
205 self._set_section(section)
206 with self._track_size():
207 n = rdataset.to_wire(name, self.output, self.compress, self.origin, **kw)
208 self.counts[section] += n
210 def add_opt(self, opt, pad=0, opt_size=0, tsig_size=0):
211 """Add *opt* to the additional section, applying padding if desired. The
212 padding will take the specified precomputed OPT size and TSIG size into
213 account.
215 Note that we don't have reliable way of knowing how big a GSS-TSIG digest
216 might be, so we we might not get an even multiple of the pad in that case."""
217 if pad:
218 ttl = opt.ttl
219 assert opt_size >= 11
220 opt_rdata = opt[0]
221 size_without_padding = self.output.tell() + opt_size + tsig_size
222 remainder = size_without_padding % pad
223 if remainder:
224 pad = b"\x00" * (pad - remainder)
225 else:
226 pad = b""
227 options = list(opt_rdata.options)
228 options.append(dns.edns.GenericOption(dns.edns.OptionType.PADDING, pad))
229 opt = _make_opt(ttl, opt_rdata.rdclass, options) # pyright: ignore
230 self.was_padded = True
231 self.add_rrset(ADDITIONAL, opt)
233 def add_edns(self, edns, ednsflags, payload, options=None):
234 """Add an EDNS OPT record to the message."""
236 # make sure the EDNS version in ednsflags agrees with edns
237 ednsflags &= 0xFF00FFFF
238 ednsflags |= edns << 16
239 opt = _make_opt(ednsflags, payload, options) # pyright: ignore
240 self.add_opt(opt)
242 def add_tsig(
243 self,
244 keyname,
245 secret,
246 fudge,
247 id,
248 tsig_error,
249 other_data,
250 request_mac,
251 algorithm=dns.tsig.default_algorithm,
252 ):
253 """Add a TSIG signature to the message."""
255 s = self.output.getvalue()
257 if isinstance(secret, dns.tsig.Key):
258 key = secret
259 else:
260 key = dns.tsig.Key(keyname, secret, algorithm)
261 tsig = _make_tsig( # pyright: ignore
262 keyname, algorithm, 0, fudge, b"", id, tsig_error, other_data
263 )
264 tsig, _ = dns.tsig.sign(s, key, tsig[0], int(time.time()), request_mac)
265 self._write_tsig(tsig, keyname)
267 def add_multi_tsig(
268 self,
269 ctx,
270 keyname,
271 secret,
272 fudge,
273 id,
274 tsig_error,
275 other_data,
276 request_mac,
277 algorithm=dns.tsig.default_algorithm,
278 ):
279 """Add a TSIG signature to the message. Unlike add_tsig(), this can be
280 used for a series of consecutive DNS envelopes, e.g. for a zone
281 transfer over TCP [RFC2845, 4.4].
283 For the first message in the sequence, give ctx=None. For each
284 subsequent message, give the ctx that was returned from the
285 add_multi_tsig() call for the previous message."""
287 s = self.output.getvalue()
289 if isinstance(secret, dns.tsig.Key):
290 key = secret
291 else:
292 key = dns.tsig.Key(keyname, secret, algorithm)
293 tsig = _make_tsig( # pyright: ignore
294 keyname, algorithm, 0, fudge, b"", id, tsig_error, other_data
295 )
296 tsig, ctx = dns.tsig.sign(
297 s, key, tsig[0], int(time.time()), request_mac, ctx, True
298 )
299 self._write_tsig(tsig, keyname)
300 return ctx
302 def _write_tsig(self, tsig, keyname):
303 if self.was_padded:
304 compress = None
305 else:
306 compress = self.compress
307 self._set_section(ADDITIONAL)
308 with self._track_size():
309 keyname.to_wire(self.output, compress, self.origin)
310 self.output.write(
311 struct.pack("!HHI", dns.rdatatype.TSIG, dns.rdataclass.ANY, 0)
312 )
313 with prefixed_length(self.output, 2):
314 tsig.to_wire(self.output)
316 self.counts[ADDITIONAL] += 1
317 with self._temporarily_seek_to(10):
318 self.output.write(struct.pack("!H", self.counts[ADDITIONAL]))
320 def write_header(self):
321 """Write the DNS message header.
323 Writing the DNS message header is done after all sections
324 have been rendered, but before the optional TSIG signature
325 is added.
326 """
328 with self._temporarily_seek_to(0):
329 self.output.write(
330 struct.pack(
331 "!HHHHHH",
332 self.id,
333 self.flags,
334 self.counts[0],
335 self.counts[1],
336 self.counts[2],
337 self.counts[3],
338 )
339 )
341 def get_wire(self):
342 """Return the wire format message."""
344 return self.output.getvalue()
346 def reserve(self, size: int) -> None:
347 """Reserve *size* bytes."""
348 if size < 0:
349 raise ValueError("reserved amount must be non-negative")
350 if size > self.max_size:
351 raise ValueError("cannot reserve more than the maximum size")
352 self.reserved += size
353 self.max_size -= size
355 def release_reserved(self) -> None:
356 """Release the reserved bytes."""
357 self.max_size += self.reserved
358 self.reserved = 0