1# Copyright (C) 2001-2017 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 rdatasets (an rdataset is a set of rdatas of a given type and class)"""
17
18import random
19from io import StringIO
20import struct
21
22import dns.exception
23import dns.rdatatype
24import dns.rdataclass
25import dns.rdata
26import dns.set
27from ._compat import string_types
28
29# define SimpleSet here for backwards compatibility
30SimpleSet = dns.set.Set
31
32
33class DifferingCovers(dns.exception.DNSException):
34    """An attempt was made to add a DNS SIG/RRSIG whose covered type
35    is not the same as that of the other rdatas in the rdataset."""
36
37
38class IncompatibleTypes(dns.exception.DNSException):
39    """An attempt was made to add DNS RR data of an incompatible type."""
40
41
42class Rdataset(dns.set.Set):
43
44    """A DNS rdataset."""
45
46    __slots__ = ['rdclass', 'rdtype', 'covers', 'ttl']
47
48    def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE, ttl=0):
49        """Create a new rdataset of the specified class and type.
50
51        *rdclass*, an ``int``, the rdataclass.
52
53        *rdtype*, an ``int``, the rdatatype.
54
55        *covers*, an ``int``, the covered rdatatype.
56
57        *ttl*, an ``int``, the TTL.
58        """
59
60        super(Rdataset, self).__init__()
61        self.rdclass = rdclass
62        self.rdtype = rdtype
63        self.covers = covers
64        self.ttl = ttl
65
66    def _clone(self):
67        obj = super(Rdataset, self)._clone()
68        obj.rdclass = self.rdclass
69        obj.rdtype = self.rdtype
70        obj.covers = self.covers
71        obj.ttl = self.ttl
72        return obj
73
74    def update_ttl(self, ttl):
75        """Perform TTL minimization.
76
77        Set the TTL of the rdataset to be the lesser of the set's current
78        TTL or the specified TTL.  If the set contains no rdatas, set the TTL
79        to the specified TTL.
80
81        *ttl*, an ``int``.
82        """
83
84        if len(self) == 0:
85            self.ttl = ttl
86        elif ttl < self.ttl:
87            self.ttl = ttl
88
89    def add(self, rd, ttl=None):
90        """Add the specified rdata to the rdataset.
91
92        If the optional *ttl* parameter is supplied, then
93        ``self.update_ttl(ttl)`` will be called prior to adding the rdata.
94
95        *rd*, a ``dns.rdata.Rdata``, the rdata
96
97        *ttl*, an ``int``, the TTL.
98
99        Raises ``dns.rdataset.IncompatibleTypes`` if the type and class
100        do not match the type and class of the rdataset.
101
102        Raises ``dns.rdataset.DifferingCovers`` if the type is a signature
103        type and the covered type does not match that of the rdataset.
104        """
105
106        #
107        # If we're adding a signature, do some special handling to
108        # check that the signature covers the same type as the
109        # other rdatas in this rdataset.  If this is the first rdata
110        # in the set, initialize the covers field.
111        #
112        if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype:
113            raise IncompatibleTypes
114        if ttl is not None:
115            self.update_ttl(ttl)
116        if self.rdtype == dns.rdatatype.RRSIG or \
117           self.rdtype == dns.rdatatype.SIG:
118            covers = rd.covers()
119            if len(self) == 0 and self.covers == dns.rdatatype.NONE:
120                self.covers = covers
121            elif self.covers != covers:
122                raise DifferingCovers
123        if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0:
124            self.clear()
125        super(Rdataset, self).add(rd)
126
127    def union_update(self, other):
128        self.update_ttl(other.ttl)
129        super(Rdataset, self).union_update(other)
130
131    def intersection_update(self, other):
132        self.update_ttl(other.ttl)
133        super(Rdataset, self).intersection_update(other)
134
135    def update(self, other):
136        """Add all rdatas in other to self.
137
138        *other*, a ``dns.rdataset.Rdataset``, the rdataset from which
139        to update.
140        """
141
142        self.update_ttl(other.ttl)
143        super(Rdataset, self).update(other)
144
145    def __repr__(self):
146        if self.covers == 0:
147            ctext = ''
148        else:
149            ctext = '(' + dns.rdatatype.to_text(self.covers) + ')'
150        return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
151               dns.rdatatype.to_text(self.rdtype) + ctext + ' rdataset>'
152
153    def __str__(self):
154        return self.to_text()
155
156    def __eq__(self, other):
157        if not isinstance(other, Rdataset):
158            return False
159        if self.rdclass != other.rdclass or \
160           self.rdtype != other.rdtype or \
161           self.covers != other.covers:
162            return False
163        return super(Rdataset, self).__eq__(other)
164
165    def __ne__(self, other):
166        return not self.__eq__(other)
167
168    def to_text(self, name=None, origin=None, relativize=True,
169                override_rdclass=None, **kw):
170        """Convert the rdataset into DNS master file format.
171
172        See ``dns.name.Name.choose_relativity`` for more information
173        on how *origin* and *relativize* determine the way names
174        are emitted.
175
176        Any additional keyword arguments are passed on to the rdata
177        ``to_text()`` method.
178
179        *name*, a ``dns.name.Name``.  If name is not ``None``, emit RRs with
180        *name* as the owner name.
181
182        *origin*, a ``dns.name.Name`` or ``None``, the origin for relative
183        names.
184
185        *relativize*, a ``bool``.  If ``True``, names will be relativized
186        to *origin*.
187        """
188
189        if name is not None:
190            name = name.choose_relativity(origin, relativize)
191            ntext = str(name)
192            pad = ' '
193        else:
194            ntext = ''
195            pad = ''
196        s = StringIO()
197        if override_rdclass is not None:
198            rdclass = override_rdclass
199        else:
200            rdclass = self.rdclass
201        if len(self) == 0:
202            #
203            # Empty rdatasets are used for the question section, and in
204            # some dynamic updates, so we don't need to print out the TTL
205            # (which is meaningless anyway).
206            #
207            s.write(u'%s%s%s %s\n' % (ntext, pad,
208                                      dns.rdataclass.to_text(rdclass),
209                                      dns.rdatatype.to_text(self.rdtype)))
210        else:
211            for rd in self:
212                s.write(u'%s%s%d %s %s %s\n' %
213                        (ntext, pad, self.ttl, dns.rdataclass.to_text(rdclass),
214                         dns.rdatatype.to_text(self.rdtype),
215                         rd.to_text(origin=origin, relativize=relativize,
216                         **kw)))
217        #
218        # We strip off the final \n for the caller's convenience in printing
219        #
220        return s.getvalue()[:-1]
221
222    def to_wire(self, name, file, compress=None, origin=None,
223                override_rdclass=None, want_shuffle=True):
224        """Convert the rdataset to wire format.
225
226        *name*, a ``dns.name.Name`` is the owner name to use.
227
228        *file* is the file where the name is emitted (typically a
229        BytesIO file).
230
231        *compress*, a ``dict``, is the compression table to use.  If
232        ``None`` (the default), names will not be compressed.
233
234        *origin* is a ``dns.name.Name`` or ``None``.  If the name is
235        relative and origin is not ``None``, then *origin* will be appended
236        to it.
237
238        *override_rdclass*, an ``int``, is used as the class instead of the
239        class of the rdataset.  This is useful when rendering rdatasets
240        associated with dynamic updates.
241
242        *want_shuffle*, a ``bool``.  If ``True``, then the order of the
243        Rdatas within the Rdataset will be shuffled before rendering.
244
245        Returns an ``int``, the number of records emitted.
246        """
247
248        if override_rdclass is not None:
249            rdclass = override_rdclass
250            want_shuffle = False
251        else:
252            rdclass = self.rdclass
253        file.seek(0, 2)
254        if len(self) == 0:
255            name.to_wire(file, compress, origin)
256            stuff = struct.pack("!HHIH", self.rdtype, rdclass, 0, 0)
257            file.write(stuff)
258            return 1
259        else:
260            if want_shuffle:
261                l = list(self)
262                random.shuffle(l)
263            else:
264                l = self
265            for rd in l:
266                name.to_wire(file, compress, origin)
267                stuff = struct.pack("!HHIH", self.rdtype, rdclass,
268                                    self.ttl, 0)
269                file.write(stuff)
270                start = file.tell()
271                rd.to_wire(file, compress, origin)
272                end = file.tell()
273                assert end - start < 65536
274                file.seek(start - 2)
275                stuff = struct.pack("!H", end - start)
276                file.write(stuff)
277                file.seek(0, 2)
278            return len(self)
279
280    def match(self, rdclass, rdtype, covers):
281        """Returns ``True`` if this rdataset matches the specified class,
282        type, and covers.
283        """
284        if self.rdclass == rdclass and \
285           self.rdtype == rdtype and \
286           self.covers == covers:
287            return True
288        return False
289
290
291def from_text_list(rdclass, rdtype, ttl, text_rdatas):
292    """Create an rdataset with the specified class, type, and TTL, and with
293    the specified list of rdatas in text format.
294
295    Returns a ``dns.rdataset.Rdataset`` object.
296    """
297
298    if isinstance(rdclass, string_types):
299        rdclass = dns.rdataclass.from_text(rdclass)
300    if isinstance(rdtype, string_types):
301        rdtype = dns.rdatatype.from_text(rdtype)
302    r = Rdataset(rdclass, rdtype)
303    r.update_ttl(ttl)
304    for t in text_rdatas:
305        rd = dns.rdata.from_text(r.rdclass, r.rdtype, t)
306        r.add(rd)
307    return r
308
309
310def from_text(rdclass, rdtype, ttl, *text_rdatas):
311    """Create an rdataset with the specified class, type, and TTL, and with
312    the specified rdatas in text format.
313
314    Returns a ``dns.rdataset.Rdataset`` object.
315    """
316
317    return from_text_list(rdclass, rdtype, ttl, text_rdatas)
318
319
320def from_rdata_list(ttl, rdatas):
321    """Create an rdataset with the specified TTL, and with
322    the specified list of rdata objects.
323
324    Returns a ``dns.rdataset.Rdataset`` object.
325    """
326
327    if len(rdatas) == 0:
328        raise ValueError("rdata list must not be empty")
329    r = None
330    for rd in rdatas:
331        if r is None:
332            r = Rdataset(rd.rdclass, rd.rdtype)
333            r.update_ttl(ttl)
334        r.add(rd)
335    return r
336
337
338def from_rdata(ttl, *rdatas):
339    """Create an rdataset with the specified TTL, and with
340    the specified rdata objects.
341
342    Returns a ``dns.rdataset.Rdataset`` object.
343    """
344
345    return from_rdata_list(ttl, rdatas)
346