Initial import
[ldd.git] / ldd / dbzone.py
CommitLineData
769e7ed9 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
18import bsddb
19import threading
20import pickle
21import logging
22
23import server
24import proto
25import rec
26import dn
27
28logger = logging.getLogger("ldd.dbzone")
29
30class 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
122def 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
130class 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