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 | |
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 |