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