s4:auth Move struct auth_usersupplied_info to a common location
[samba.git] / lib / dnspython / dns / rdata.py
1 # Copyright (C) 2001-2007, 2009, 2010 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
16 """DNS rdata.
17
18 @var _rdata_modules: A dictionary mapping a (rdclass, rdtype) tuple to
19 the module which implements that type.
20 @type _rdata_modules: dict
21 @var _module_prefix: The prefix to use when forming modules names.  The
22 default is 'dns.rdtypes'.  Changing this value will break the library.
23 @type _module_prefix: string
24 @var _hex_chunk: At most this many octets that will be represented in each
25 chunk of hexstring that _hexify() produces before whitespace occurs.
26 @type _hex_chunk: int"""
27
28 import cStringIO
29
30 import dns.exception
31 import dns.rdataclass
32 import dns.rdatatype
33 import dns.tokenizer
34
35 _hex_chunksize = 32
36
37 def _hexify(data, chunksize=None):
38     """Convert a binary string into its hex encoding, broken up into chunks
39     of I{chunksize} characters separated by a space.
40
41     @param data: the binary string
42     @type data: string
43     @param chunksize: the chunk size.  Default is L{dns.rdata._hex_chunksize}
44     @rtype: string
45     """
46
47     if chunksize is None:
48         chunksize = _hex_chunksize
49     hex = data.encode('hex_codec')
50     l = len(hex)
51     if l > chunksize:
52         chunks = []
53         i = 0
54         while i < l:
55             chunks.append(hex[i : i + chunksize])
56             i += chunksize
57         hex = ' '.join(chunks)
58     return hex
59
60 _base64_chunksize = 32
61
62 def _base64ify(data, chunksize=None):
63     """Convert a binary string into its base64 encoding, broken up into chunks
64     of I{chunksize} characters separated by a space.
65
66     @param data: the binary string
67     @type data: string
68     @param chunksize: the chunk size.  Default is
69     L{dns.rdata._base64_chunksize}
70     @rtype: string
71     """
72
73     if chunksize is None:
74         chunksize = _base64_chunksize
75     b64 = data.encode('base64_codec')
76     b64 = b64.replace('\n', '')
77     l = len(b64)
78     if l > chunksize:
79         chunks = []
80         i = 0
81         while i < l:
82             chunks.append(b64[i : i + chunksize])
83             i += chunksize
84         b64 = ' '.join(chunks)
85     return b64
86
87 __escaped = {
88     '"' : True,
89     '\\' : True,
90     }
91
92 def _escapify(qstring):
93     """Escape the characters in a quoted string which need it.
94
95     @param qstring: the string
96     @type qstring: string
97     @returns: the escaped string
98     @rtype: string
99     """
100
101     text = ''
102     for c in qstring:
103         if c in __escaped:
104             text += '\\' + c
105         elif ord(c) >= 0x20 and ord(c) < 0x7F:
106             text += c
107         else:
108             text += '\\%03d' % ord(c)
109     return text
110
111 def _truncate_bitmap(what):
112     """Determine the index of greatest byte that isn't all zeros, and
113     return the bitmap that contains all the bytes less than that index.
114
115     @param what: a string of octets representing a bitmap.
116     @type what: string
117     @rtype: string
118     """
119
120     for i in xrange(len(what) - 1, -1, -1):
121         if what[i] != '\x00':
122             break
123     return ''.join(what[0 : i + 1])
124
125 class Rdata(object):
126     """Base class for all DNS rdata types.
127     """
128
129     __slots__ = ['rdclass', 'rdtype']
130
131     def __init__(self, rdclass, rdtype):
132         """Initialize an rdata.
133         @param rdclass: The rdata class
134         @type rdclass: int
135         @param rdtype: The rdata type
136         @type rdtype: int
137         """
138
139         self.rdclass = rdclass
140         self.rdtype = rdtype
141
142     def covers(self):
143         """DNS SIG/RRSIG rdatas apply to a specific type; this type is
144         returned by the covers() function.  If the rdata type is not
145         SIG or RRSIG, dns.rdatatype.NONE is returned.  This is useful when
146         creating rdatasets, allowing the rdataset to contain only RRSIGs
147         of a particular type, e.g. RRSIG(NS).
148         @rtype: int
149         """
150
151         return dns.rdatatype.NONE
152
153     def extended_rdatatype(self):
154         """Return a 32-bit type value, the least significant 16 bits of
155         which are the ordinary DNS type, and the upper 16 bits of which are
156         the "covered" type, if any.
157         @rtype: int
158         """
159
160         return self.covers() << 16 | self.rdtype
161
162     def to_text(self, origin=None, relativize=True, **kw):
163         """Convert an rdata to text format.
164         @rtype: string
165         """
166         raise NotImplementedError
167
168     def to_wire(self, file, compress = None, origin = None):
169         """Convert an rdata to wire format.
170         @rtype: string
171         """
172
173         raise NotImplementedError
174
175     def to_digestable(self, origin = None):
176         """Convert rdata to a format suitable for digesting in hashes.  This
177         is also the DNSSEC canonical form."""
178         f = cStringIO.StringIO()
179         self.to_wire(f, None, origin)
180         return f.getvalue()
181
182     def validate(self):
183         """Check that the current contents of the rdata's fields are
184         valid.  If you change an rdata by assigning to its fields,
185         it is a good idea to call validate() when you are done making
186         changes.
187         """
188         dns.rdata.from_text(self.rdclass, self.rdtype, self.to_text())
189
190     def __repr__(self):
191         covers = self.covers()
192         if covers == dns.rdatatype.NONE:
193             ctext = ''
194         else:
195             ctext = '(' + dns.rdatatype.to_text(covers) + ')'
196         return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
197                dns.rdatatype.to_text(self.rdtype) + ctext + ' rdata: ' + \
198                str(self) + '>'
199
200     def __str__(self):
201         return self.to_text()
202
203     def _cmp(self, other):
204         """Compare an rdata with another rdata of the same rdtype and
205         rdclass.  Return < 0 if self < other in the DNSSEC ordering,
206         0 if self == other, and > 0 if self > other.
207         """
208
209         raise NotImplementedError
210
211     def __eq__(self, other):
212         if not isinstance(other, Rdata):
213             return False
214         if self.rdclass != other.rdclass or \
215            self.rdtype != other.rdtype:
216             return False
217         return self._cmp(other) == 0
218
219     def __ne__(self, other):
220         if not isinstance(other, Rdata):
221             return True
222         if self.rdclass != other.rdclass or \
223            self.rdtype != other.rdtype:
224             return True
225         return self._cmp(other) != 0
226
227     def __lt__(self, other):
228         if not isinstance(other, Rdata) or \
229                self.rdclass != other.rdclass or \
230                self.rdtype != other.rdtype:
231             return NotImplemented
232         return self._cmp(other) < 0
233
234     def __le__(self, other):
235         if not isinstance(other, Rdata) or \
236                self.rdclass != other.rdclass or \
237                self.rdtype != other.rdtype:
238             return NotImplemented
239         return self._cmp(other) <= 0
240
241     def __ge__(self, other):
242         if not isinstance(other, Rdata) or \
243                self.rdclass != other.rdclass or \
244                self.rdtype != other.rdtype:
245             return NotImplemented
246         return self._cmp(other) >= 0
247
248     def __gt__(self, other):
249         if not isinstance(other, Rdata) or \
250                self.rdclass != other.rdclass or \
251                self.rdtype != other.rdtype:
252             return NotImplemented
253         return self._cmp(other) > 0
254
255     def from_text(cls, rdclass, rdtype, tok, origin = None, relativize = True):
256         """Build an rdata object from text format.
257
258         @param rdclass: The rdata class
259         @type rdclass: int
260         @param rdtype: The rdata type
261         @type rdtype: int
262         @param tok: The tokenizer
263         @type tok: dns.tokenizer.Tokenizer
264         @param origin: The origin to use for relative names
265         @type origin: dns.name.Name
266         @param relativize: should names be relativized?
267         @type relativize: bool
268         @rtype: dns.rdata.Rdata instance
269         """
270
271         raise NotImplementedError
272
273     from_text = classmethod(from_text)
274
275     def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin = None):
276         """Build an rdata object from wire format
277
278         @param rdclass: The rdata class
279         @type rdclass: int
280         @param rdtype: The rdata type
281         @type rdtype: int
282         @param wire: The wire-format message
283         @type wire: string
284         @param current: The offet in wire of the beginning of the rdata.
285         @type current: int
286         @param rdlen: The length of the wire-format rdata
287         @type rdlen: int
288         @param origin: The origin to use for relative names
289         @type origin: dns.name.Name
290         @rtype: dns.rdata.Rdata instance
291         """
292
293         raise NotImplementedError
294
295     from_wire = classmethod(from_wire)
296
297     def choose_relativity(self, origin = None, relativize = True):
298         """Convert any domain names in the rdata to the specified
299         relativization.
300         """
301
302         pass
303
304
305 class GenericRdata(Rdata):
306     """Generate Rdata Class
307
308     This class is used for rdata types for which we have no better
309     implementation.  It implements the DNS "unknown RRs" scheme.
310     """
311
312     __slots__ = ['data']
313
314     def __init__(self, rdclass, rdtype, data):
315         super(GenericRdata, self).__init__(rdclass, rdtype)
316         self.data = data
317
318     def to_text(self, origin=None, relativize=True, **kw):
319         return r'\# %d ' % len(self.data) + _hexify(self.data)
320
321     def from_text(cls, rdclass, rdtype, tok, origin = None, relativize = True):
322         token = tok.get()
323         if not token.is_identifier() or token.value != '\#':
324             raise dns.exception.SyntaxError(r'generic rdata does not start with \#')
325         length = tok.get_int()
326         chunks = []
327         while 1:
328             token = tok.get()
329             if token.is_eol_or_eof():
330                 break
331             chunks.append(token.value)
332         hex = ''.join(chunks)
333         data = hex.decode('hex_codec')
334         if len(data) != length:
335             raise dns.exception.SyntaxError('generic rdata hex data has wrong length')
336         return cls(rdclass, rdtype, data)
337
338     from_text = classmethod(from_text)
339
340     def to_wire(self, file, compress = None, origin = None):
341         file.write(self.data)
342
343     def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin = None):
344         return cls(rdclass, rdtype, wire[current : current + rdlen])
345
346     from_wire = classmethod(from_wire)
347
348     def _cmp(self, other):
349         return cmp(self.data, other.data)
350
351 _rdata_modules = {}
352 _module_prefix = 'dns.rdtypes'
353
354 def get_rdata_class(rdclass, rdtype):
355
356     def import_module(name):
357         mod = __import__(name)
358         components = name.split('.')
359         for comp in components[1:]:
360             mod = getattr(mod, comp)
361         return mod
362
363     mod = _rdata_modules.get((rdclass, rdtype))
364     rdclass_text = dns.rdataclass.to_text(rdclass)
365     rdtype_text = dns.rdatatype.to_text(rdtype)
366     rdtype_text = rdtype_text.replace('-', '_')
367     if not mod:
368         mod = _rdata_modules.get((dns.rdatatype.ANY, rdtype))
369         if not mod:
370             try:
371                 mod = import_module('.'.join([_module_prefix,
372                                               rdclass_text, rdtype_text]))
373                 _rdata_modules[(rdclass, rdtype)] = mod
374             except ImportError:
375                 try:
376                     mod = import_module('.'.join([_module_prefix,
377                                                   'ANY', rdtype_text]))
378                     _rdata_modules[(dns.rdataclass.ANY, rdtype)] = mod
379                 except ImportError:
380                     mod = None
381     if mod:
382         cls = getattr(mod, rdtype_text)
383     else:
384         cls = GenericRdata
385     return cls
386
387 def from_text(rdclass, rdtype, tok, origin = None, relativize = True):
388     """Build an rdata object from text format.
389
390     This function attempts to dynamically load a class which
391     implements the specified rdata class and type.  If there is no
392     class-and-type-specific implementation, the GenericRdata class
393     is used.
394
395     Once a class is chosen, its from_text() class method is called
396     with the parameters to this function.
397
398     @param rdclass: The rdata class
399     @type rdclass: int
400     @param rdtype: The rdata type
401     @type rdtype: int
402     @param tok: The tokenizer
403     @type tok: dns.tokenizer.Tokenizer
404     @param origin: The origin to use for relative names
405     @type origin: dns.name.Name
406     @param relativize: Should names be relativized?
407     @type relativize: bool
408     @rtype: dns.rdata.Rdata instance"""
409
410     if isinstance(tok, str):
411         tok = dns.tokenizer.Tokenizer(tok)
412     cls = get_rdata_class(rdclass, rdtype)
413     if cls != GenericRdata:
414         # peek at first token
415         token = tok.get()
416         tok.unget(token)
417         if token.is_identifier() and \
418            token.value == r'\#':
419             #
420             # Known type using the generic syntax.  Extract the
421             # wire form from the generic syntax, and then run
422             # from_wire on it.
423             #
424             rdata = GenericRdata.from_text(rdclass, rdtype, tok, origin,
425                                            relativize)
426             return from_wire(rdclass, rdtype, rdata.data, 0, len(rdata.data),
427                              origin)
428     return cls.from_text(rdclass, rdtype, tok, origin, relativize)
429
430 def from_wire(rdclass, rdtype, wire, current, rdlen, origin = None):
431     """Build an rdata object from wire format
432
433     This function attempts to dynamically load a class which
434     implements the specified rdata class and type.  If there is no
435     class-and-type-specific implementation, the GenericRdata class
436     is used.
437
438     Once a class is chosen, its from_wire() class method is called
439     with the parameters to this function.
440
441     @param rdclass: The rdata class
442     @type rdclass: int
443     @param rdtype: The rdata type
444     @type rdtype: int
445     @param wire: The wire-format message
446     @type wire: string
447     @param current: The offet in wire of the beginning of the rdata.
448     @type current: int
449     @param rdlen: The length of the wire-format rdata
450     @type rdlen: int
451     @param origin: The origin to use for relative names
452     @type origin: dns.name.Name
453     @rtype: dns.rdata.Rdata instance"""
454
455     cls = get_rdata_class(rdclass, rdtype)
456     return cls.from_wire(rdclass, rdtype, wire, current, rdlen, origin)