Initial import
[ldd.git] / ldd / dbzone.py
1 #    ldd - DNS implementation in Python
2 #    Copyright (C) 2006 Fredrik Tolf <fredrik@dolda2000.com>
3 #
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any later version.
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program; if not, write to the Free Software
16 #    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18 import bsddb
19 import threading
20 import pickle
21 import logging
22
23 import server
24 import proto
25 import rec
26 import dn
27
28 logger = logging.getLogger("ldd.dbzone")
29
30 class dnsdb:
31     def __init__(self, dbdir, dbfile):
32         self.env = bsddb.db.DBEnv()
33         self.env.open(dbdir, bsddb.db.DB_JOINENV | bsddb.db.DB_THREAD)
34         self.db = bsddb.db.DB()
35         self.db.open(dbdir + "/" + dbfile, flags = bsddb.db.DB_THREAD)
36
37     def create(self, dbdir, dbfile):
38         env = bsddb.db.DBEnv()
39         env.open(dbdir, bsddb.db.DB_CREATE | bsddb.db.DB_EXCL | bsddb.db.DB_INIT_MPOOL | bsddb.db.DB_INIT_CDB | bsddb.db.DB_THREAD)
40         db = bsddb.db.DB()
41         db.open(dbdir + "/" + dbfile, dbtype = bsddb.db.DB_HASH, flags = bsddb.db.DB_CREATE | bsddb.db.DB_EXCL | bsddb.db.DB_THREAD)
42         db.close()
43         env.close()
44     create = classmethod(create)
45
46     def close(self):
47         self.db.close()
48         self.env.close()
49
50     def decoderecord(self, name, record):
51         set = pickle.loads(record)
52         rrset = []
53         for cur in set:
54             head = rec.rrhead(name, cur[0])
55             data = cur[2]
56             newrr = rec.rr(head, cur[1], data)
57             newrr.setflags(cur[3])
58             rrset += [newrr]
59         return rrset
60
61     def encoderecord(self, rrset):
62         set = []
63         for rr in rrset:
64             set += [(rr.head.rtype, rr.ttl, rr.data, rr.flags)]
65         return pickle.dumps(set)
66
67     def lookup(self, name):
68         record = self.db.get(str(name))
69         if record is None:
70             return None
71         return self.decoderecord(name, record)
72
73     def set(self, name, rrset):
74         self.db.put(str(name), self.encoderecord(rrset))
75         return True
76     
77     def hasname(self, name):
78         record = self.db.get(str(name))
79         return record is not None
80
81     def rmname(self, name):
82         try:
83             self.db.delete(str(name))
84         except bsddb.db.DBNotFoundError:
85             return False
86         return True
87
88     def rmrtype(self, name, rtype):
89         if type(rtype) == str:
90             rtype = rec.rtypebyname(rtype)
91         rrset = self.lookup(name)
92         if rrset is None:
93             return False
94         for rr in rrset:
95             if rr.head.rtype == rtype:
96                 rrset.remove(rr)
97         self.set(name, rrset)
98         return True
99
100     def addrr(self, name, rr):
101         rrset = self.lookup(name)
102         if rrset is None:
103             rrset = []
104         rrset += [rr]
105         self.set(name, rrset)
106         return True
107
108     def listnames(self):
109         cursor = self.db.cursor()
110         ret = cursor.first()
111         if ret is not None:
112             name, record = ret
113             yield name
114             while True:
115                 ret = cursor.next()
116                 if ret is None:
117                     break
118                 name, record = ret
119                 yield name
120         cursor.close()
121
122 def rootify(rrset, origin):
123     for rr in rrset:
124         if not rr.head.name.rooted:
125             rr.head.name += origin
126         for dname, dval in rr.data.rdata.items():
127             if isinstance(dval, dn.domainname) and not dval.rooted:
128                 rr.data.rdata[dname] += origin
129
130 class dbhandler(server.handler):
131     def __init__(self, dbdir, dbfile):
132         self.db = dnsdb(dbdir, dbfile)
133         self.doddns = False
134         self.authkeys = []
135
136     def handle(self, query, pkt, origin):
137         resp = proto.responsefor(pkt)
138         if pkt.opcode == proto.QUERY:
139             rrset = self.db.lookup(query.name)
140             if rrset is None and query.name in origin:
141                 rrset = self.db.lookup(query.name - origin)
142             if rrset is None:
143                 return None
144             rootify(rrset, origin)
145             resp.anlist = [rr for rr in rrset if rr.head.rtype == query.rtype or rr.head.istype("CNAME")]
146             return resp
147         if pkt.opcode == proto.UPDATE:
148             logger.debug("got DDNS request")
149             if len(pkt.qlist) != 1 or not pkt.qlist[0].istype("SOA"):
150                 resp.rescode = proto.FORMERR
151                 return resp
152             if pkt.qlist[0].name != origin:
153                 resp.rescode = proto.NOTAUTH
154                 return resp
155
156             # Check prerequisites
157             for rr in pkt.anlist:
158                 if rr.ttl != 0:
159                     resp.rescode = proto.FORMERR
160                     return resp
161                 if rr.head.name not in origin:
162                     resp.rescode = proto.NOTZONE
163                     return resp
164                 myname = rr.head.name - origin
165                 rrset = self.db.lookup(myname)
166                 if rr.head.rclass == rec.CLASSANY:
167                     if rr.data is not None:
168                         resp.rescode = proto.FORMERR
169                         return resp
170                     if rr.head.rtype == proto.QTANY:
171                         if rrset is None:
172                             resp.rescode = proto.NXDOMAIN
173                             return resp
174                     else:
175                         if rrset is not None:
176                             for rr2 in rrset:
177                                 if rr2.head.name == myname and rr.head.rtype == rr2.head.rtype:
178                                     break
179                             else:
180                                 resp.rescode = proto.NXRRSET
181                                 return resp
182                 elif rr.head.rclass == rec.CLASSNONE:
183                     if rr.data is not None:
184                         resp.rescode = proto.FORMERR
185                         return resp
186                     if rr.head.rtype == proto.QTANY:
187                         if rrset is not None:
188                             resp.rescode = proto.YXDOMAIN
189                             return resp
190                     else:
191                         if rrset is not None:
192                             for rr2 in rrset:
193                                 if rr2.head.name == myname and rr.head.rtype == rr2.head.rtype:
194                                     resp.rescode = proto.YXRRSET
195                                     return resp
196                 elif rr.head.rclass == rec.CLASSIN:
197                     if rrset is not None:
198                         for rr2 in rrset:
199                             if rr2.head.name == myname and rr.head.rtype == rr2.head.rtype and rr.data == rr2.data:
200                                 break
201                         else:
202                             resp.rescode = proto.NXRRSET
203                             return resp
204                 else:
205                     resp.rescode = FORMERR
206                     return resp
207
208             # Check for permission
209             if not self.doddns:
210                 resp.rescode = proto.REFUSED
211                 return resp
212             if type(self.authkeys) == list:
213                 if pkt.tsigctx is None:
214                     resp.rescode = proto.REFUSED
215                     return resp
216                 if pkt.tsigctx.error != 0:
217                     resp.rescode = proto.NOTAUTH
218                     return resp
219                 if pkt.tsigctx.key not in self.authkeys:
220                     resp.rescode = proto.REFUSED
221                     return resp
222             elif type(self.authkeys) == None:
223                 authorized = True
224
225             # Do precheck on updates
226             for rr in pkt.aulist:
227                 if rr.head.name not in origin:
228                     resp.rescode = proto.NOTZONE
229                     return resp
230                 if rr.head.rclass == rec.CLASSIN:
231                     if rr.head.rtype == proto.QTANY or rr.data is None:
232                         resp.rescode = proto.FORMERR
233                         return resp
234                 elif rr.head.rclass == rec.CLASSANY:
235                     if rr.data is not None:
236                         resp.rescode = proto.FORMERR
237                         return resp
238                 elif rr.head.rclass == rec.CLASSNONE:
239                     if rr.head.rtype == proto.QTANY or rr.ttl != 0 or rr.data is None:
240                         resp.rescode = proto.FORMERR
241                         return resp
242                 else:
243                     resp.rescode = proto.FORMERR
244                     return resp
245
246             # Perform updates
247             for rr in pkt.aulist:
248                 myname = rr.head.name - origin
249                 if rr.head.rclass == rec.CLASSIN:
250                     logger.info("adding rr (%s)", rr)
251                     self.db.addrr(myname, rr)
252                 elif rr.head.rclass == rec.CLASSANY:
253                     if rr.head.rtype == proto.QTANY:
254                         logger.info("removing rrset (%s)", rr.head.name)
255                         self.db.rmname(myname)
256                     else:
257                         logger.info("removing rrset (%s, %s)", rr.head.name, rr.head.rtype)
258                         self.db.rmrtype(myname, rr.head.rtype)
259                 elif rr.head.rclass == rec.CLASSNONE:
260                     logger.info("removing rr (%s)", rr)
261                     rrset = self.db.lookup(myname)
262                     changed = False
263                     if rrset is not None:
264                         for rr2 in rrset:
265                             if rr2.head == rr.head and rr2.data == rr.data:
266                                 rrset.remove(rr2)
267                                 changed = True
268                         self.db.set(myname, rrset)
269
270             return resp
271             
272         resp.rescode = proto.NOTIMP
273         return resp