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
19the module which implements that type.
20@type _rdata_modules: dict
21@var _module_prefix: The prefix to use when forming modules names.  The
22default 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
25chunk of hexstring that _hexify() produces before whitespace occurs.
26@type _hex_chunk: int"""
27
28import cStringIO
29
30import dns.exception
31import dns.rdataclass
32import dns.rdatatype
33import dns.tokenizer
34
35_hex_chunksize = 32
36
37def _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
62def _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
92def _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
111def _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
125class 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
305class 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
354def 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
387def 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
430def 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)
457