1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
2
3# Copyright (C) 2003-2007, 2009-2011 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.
17
18"""DNS Dynamic Update Support"""
19
20
21import dns.message
22import dns.name
23import dns.opcode
24import dns.rdata
25import dns.rdataclass
26import dns.rdataset
27import dns.tsig
28
29
30class UpdateSection(dns.enum.IntEnum):
31    """Update sections"""
32    ZONE = 0
33    PREREQ = 1
34    UPDATE = 2
35    ADDITIONAL = 3
36
37    @classmethod
38    def _maximum(cls):
39        return 3
40
41globals().update(UpdateSection.__members__)
42
43
44class UpdateMessage(dns.message.Message):
45
46    _section_enum = UpdateSection
47
48    def __init__(self, zone=None, rdclass=dns.rdataclass.IN, keyring=None,
49                 keyname=None, keyalgorithm=dns.tsig.default_algorithm,
50                 id=None):
51        """Initialize a new DNS Update object.
52
53        See the documentation of the Message class for a complete
54        description of the keyring dictionary.
55
56        *zone*, a ``dns.name.Name``, ``str``, or ``None``, the zone
57        which is being updated.  ``None`` should only be used by dnspython's
58        message constructors, as a zone is required for the convenience
59        methods like ``add()``, ``replace()``, etc.
60
61        *rdclass*, an ``int`` or ``str``, the class of the zone.
62
63        The *keyring*, *keyname*, and *keyalgorithm* parameters are passed to
64        ``use_tsig()``; see its documentation for details.
65        """
66        super().__init__(id=id)
67        self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE)
68        if isinstance(zone, str):
69            zone = dns.name.from_text(zone)
70        self.origin = zone
71        rdclass = dns.rdataclass.RdataClass.make(rdclass)
72        self.zone_rdclass = rdclass
73        if self.origin:
74            self.find_rrset(self.zone, self.origin, rdclass, dns.rdatatype.SOA,
75                            create=True, force_unique=True)
76        if keyring is not None:
77            self.use_tsig(keyring, keyname, algorithm=keyalgorithm)
78
79    @property
80    def zone(self):
81        """The zone section."""
82        return self.sections[0]
83
84    @zone.setter
85    def zone(self, v):
86        self.sections[0] = v
87
88    @property
89    def prerequisite(self):
90        """The prerequisite section."""
91        return self.sections[1]
92
93    @prerequisite.setter
94    def prerequisite(self, v):
95        self.sections[1] = v
96
97    @property
98    def update(self):
99        """The update section."""
100        return self.sections[2]
101
102    @update.setter
103    def update(self, v):
104        self.sections[2] = v
105
106    def _add_rr(self, name, ttl, rd, deleting=None, section=None):
107        """Add a single RR to the update section."""
108
109        if section is None:
110            section = self.update
111        covers = rd.covers()
112        rrset = self.find_rrset(section, name, self.zone_rdclass, rd.rdtype,
113                                covers, deleting, True, True)
114        rrset.add(rd, ttl)
115
116    def _add(self, replace, section, name, *args):
117        """Add records.
118
119        *replace* is the replacement mode.  If ``False``,
120        RRs are added to an existing RRset; if ``True``, the RRset
121        is replaced with the specified contents.  The second
122        argument is the section to add to.  The third argument
123        is always a name.  The other arguments can be:
124
125                - rdataset...
126
127                - ttl, rdata...
128
129                - ttl, rdtype, string...
130        """
131
132        if isinstance(name, str):
133            name = dns.name.from_text(name, None)
134        if isinstance(args[0], dns.rdataset.Rdataset):
135            for rds in args:
136                if replace:
137                    self.delete(name, rds.rdtype)
138                for rd in rds:
139                    self._add_rr(name, rds.ttl, rd, section=section)
140        else:
141            args = list(args)
142            ttl = int(args.pop(0))
143            if isinstance(args[0], dns.rdata.Rdata):
144                if replace:
145                    self.delete(name, args[0].rdtype)
146                for rd in args:
147                    self._add_rr(name, ttl, rd, section=section)
148            else:
149                rdtype = dns.rdatatype.RdataType.make(args.pop(0))
150                if replace:
151                    self.delete(name, rdtype)
152                for s in args:
153                    rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,
154                                             self.origin)
155                    self._add_rr(name, ttl, rd, section=section)
156
157    def add(self, name, *args):
158        """Add records.
159
160        The first argument is always a name.  The other
161        arguments can be:
162
163                - rdataset...
164
165                - ttl, rdata...
166
167                - ttl, rdtype, string...
168        """
169
170        self._add(False, self.update, name, *args)
171
172    def delete(self, name, *args):
173        """Delete records.
174
175        The first argument is always a name.  The other
176        arguments can be:
177
178                - *empty*
179
180                - rdataset...
181
182                - rdata...
183
184                - rdtype, [string...]
185        """
186
187        if isinstance(name, str):
188            name = dns.name.from_text(name, None)
189        if len(args) == 0:
190            self.find_rrset(self.update, name, dns.rdataclass.ANY,
191                            dns.rdatatype.ANY, dns.rdatatype.NONE,
192                            dns.rdatatype.ANY, True, True)
193        elif isinstance(args[0], dns.rdataset.Rdataset):
194            for rds in args:
195                for rd in rds:
196                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)
197        else:
198            args = list(args)
199            if isinstance(args[0], dns.rdata.Rdata):
200                for rd in args:
201                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)
202            else:
203                rdtype = dns.rdatatype.RdataType.make(args.pop(0))
204                if len(args) == 0:
205                    self.find_rrset(self.update, name,
206                                    self.zone_rdclass, rdtype,
207                                    dns.rdatatype.NONE,
208                                    dns.rdataclass.ANY,
209                                    True, True)
210                else:
211                    for s in args:
212                        rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,
213                                                 self.origin)
214                        self._add_rr(name, 0, rd, dns.rdataclass.NONE)
215
216    def replace(self, name, *args):
217        """Replace records.
218
219        The first argument is always a name.  The other
220        arguments can be:
221
222                - rdataset...
223
224                - ttl, rdata...
225
226                - ttl, rdtype, string...
227
228        Note that if you want to replace the entire node, you should do
229        a delete of the name followed by one or more calls to add.
230        """
231
232        self._add(True, self.update, name, *args)
233
234    def present(self, name, *args):
235        """Require that an owner name (and optionally an rdata type,
236        or specific rdataset) exists as a prerequisite to the
237        execution of the update.
238
239        The first argument is always a name.
240        The other arguments can be:
241
242                - rdataset...
243
244                - rdata...
245
246                - rdtype, string...
247        """
248
249        if isinstance(name, str):
250            name = dns.name.from_text(name, None)
251        if len(args) == 0:
252            self.find_rrset(self.prerequisite, name,
253                            dns.rdataclass.ANY, dns.rdatatype.ANY,
254                            dns.rdatatype.NONE, None,
255                            True, True)
256        elif isinstance(args[0], dns.rdataset.Rdataset) or \
257            isinstance(args[0], dns.rdata.Rdata) or \
258                len(args) > 1:
259            if not isinstance(args[0], dns.rdataset.Rdataset):
260                # Add a 0 TTL
261                args = list(args)
262                args.insert(0, 0)
263            self._add(False, self.prerequisite, name, *args)
264        else:
265            rdtype = dns.rdatatype.RdataType.make(args[0])
266            self.find_rrset(self.prerequisite, name,
267                            dns.rdataclass.ANY, rdtype,
268                            dns.rdatatype.NONE, None,
269                            True, True)
270
271    def absent(self, name, rdtype=None):
272        """Require that an owner name (and optionally an rdata type) does
273        not exist as a prerequisite to the execution of the update."""
274
275        if isinstance(name, str):
276            name = dns.name.from_text(name, None)
277        if rdtype is None:
278            self.find_rrset(self.prerequisite, name,
279                            dns.rdataclass.NONE, dns.rdatatype.ANY,
280                            dns.rdatatype.NONE, None,
281                            True, True)
282        else:
283            rdtype = dns.rdatatype.RdataType.make(rdtype)
284            self.find_rrset(self.prerequisite, name,
285                            dns.rdataclass.NONE, rdtype,
286                            dns.rdatatype.NONE, None,
287                            True, True)
288
289    def _get_one_rr_per_rrset(self, value):
290        # Updates are always one_rr_per_rrset
291        return True
292
293    def _parse_rr_header(self, section, name, rdclass, rdtype):
294        deleting = None
295        empty = False
296        if section == UpdateSection.ZONE:
297            if dns.rdataclass.is_metaclass(rdclass) or \
298               rdtype != dns.rdatatype.SOA or \
299               self.zone:
300                raise dns.exception.FormError
301        else:
302            if not self.zone:
303                raise dns.exception.FormError
304            if rdclass in (dns.rdataclass.ANY, dns.rdataclass.NONE):
305                deleting = rdclass
306                rdclass = self.zone[0].rdclass
307                empty = (deleting == dns.rdataclass.ANY or
308                         section == UpdateSection.PREREQ)
309        return (rdclass, rdtype, deleting, empty)
310
311# backwards compatibility
312Update = UpdateMessage
313