]> git.ipfire.org Git - thirdparty/pdns.git/blob - modules/mydnsbackend/mydnsbackend.cc
ba583cb13bc1bd9c58946ffd5e64fc1240db8d71
[thirdparty/pdns.git] / modules / mydnsbackend / mydnsbackend.cc
1 /*
2 * PowerDNS backend module for MyDNS style databases
3 * Author: Jonathan Oddy (Hostway UK) <jonathan@woaf.net>
4 * By PowerDNS.COM BV
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 2
8 * as published by the Free Software Foundation.
9 *
10 * Additionally, the license of this program contains a special
11 * exception which allows to distribute the program in binary form when
12 * it is linked against OpenSSL.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 *
23 * The schema used by MyDNS isn't suitable for retrieving results with a single
24 * query. This means that existing PowerDNS backends are unable to make use of
25 * the schema without lame hackery (or awful performance.) This module does
26 * the nasty lookup logic required to make use of the schema, and should be as
27 * tolerant as MyDNS when it comes to things being fully qualified or not.
28 *
29 * A known "bug" is that AXFRs will fail if your rr table contains invalid
30 * junk. I'm not sure this is really a bug, if you've decided to put free-form
31 * text in your data for an A record you have bigger issues.
32 *
33 * I'd advise avoiding the MyDNS schema if at all possible as the query count
34 * for even simple lookups is daft. It's quite trivial to craft a request
35 * that'll require 128 database queries to answer with a servfail!
36 *
37 * If you do not know what mydns is: http://mydns.bboy.net/
38 */
39
40 #ifdef HAVE_CONFIG_H
41 #include "config.h"
42 #endif
43 #include <string>
44 #include <map>
45 #include <unistd.h>
46 #include <stdlib.h>
47 #include <sstream>
48
49 #include "pdns/namespaces.hh"
50
51 #include "pdns/dns.hh"
52 #include "pdns/dnsbackend.hh"
53 #include "mydnsbackend.hh"
54 #include "pdns/dnspacket.hh"
55 #include "pdns/ueberbackend.hh"
56 #include "pdns/pdnsexception.hh"
57 #include "pdns/logger.hh"
58 #include "pdns/arguments.hh"
59
60 #include <modules/gmysqlbackend/smysql.hh>
61
62 static string backendName="[MyDNSbackend]";
63
64 MyDNSBackend::MyDNSBackend(const string &suffix) {
65 setArgPrefix("mydns"+suffix);
66
67 d_domainIdQuery_stmt = NULL;
68 d_domainNoIdQuery_stmt = NULL;
69 d_listQuery_stmt = NULL;
70 d_soaQuery_stmt = NULL;
71 d_basicQuery_stmt = NULL;
72 d_anyQuery_stmt = NULL;
73 d_query_stmt = NULL;
74
75 try {
76 d_db = new SMySQL(getArg("dbname"),
77 getArg("host"),
78 getArgAsNum("port"),
79 getArg("socket"),
80 getArg("user"),
81 getArg("password"));
82 d_db->setLog(::arg().mustDo("query-logging"));
83 }
84 catch(SSqlException &e) {
85 L<<Logger::Error<<backendName<<" Connection failed: "<<e.txtReason()<<endl;
86 throw PDNSException(backendName+"Unable to launch connection: "+e.txtReason());
87 }
88
89 string rrtable=getArg("rr-table");
90 string soatable=getArg("soa-table");
91 string rrwhere=(mustDo("rr-active")?"(active = '1' or active = 'Y') and ":"")+getArg("rr-where");
92 string soawhere=(mustDo("soa-active")?"(active = '1' or active = 'Y') and ":"")+getArg("soa-where");
93
94 if (soatable.empty()) { throw PDNSException("SOA Table must not be empty"); }
95 if (rrtable.empty()) { throw PDNSException("Records table must not be empty"); }
96
97 d_useminimalttl=mustDo("use-minimal-ttl");
98 d_minimum=0;
99
100 L<<Logger::Warning<<backendName<<" Connection successful"<<endl;
101
102 try {
103
104 string domainIdQuery = "SELECT origin, minimum FROM `"+soatable+"` WHERE id = ?";
105 string domainNoIdQuery = "SELECT id, origin, minimum FROM `"+soatable+"` WHERE origin = ?";
106 string soaQuery = "SELECT id, mbox, serial, ns, refresh, retry, expire, minimum, ttl FROM `"+soatable+"` WHERE origin = ?";
107
108 if (!soawhere.empty()) {
109 domainIdQuery += " AND " + soawhere;
110 domainNoIdQuery += " AND " + soawhere;
111 soaQuery += " AND "+soawhere;
112 }
113
114 d_domainIdQuery_stmt = d_db->prepare(domainIdQuery, 1);
115 d_domainNoIdQuery_stmt = d_db->prepare(domainNoIdQuery, 1);
116 d_soaQuery_stmt = d_db->prepare(soaQuery, 1);
117
118 string listQuery = "SELECT type, data, aux, ttl, zone, name FROM `"+rrtable+"` WHERE zone = ?";
119 string basicQuery = "SELECT type, data, aux, ttl, zone FROM `"+rrtable+"` WHERE zone = ? AND (name = ? OR name = ?) AND type = ?";
120 string anyQuery = "(SELECT type, data, aux, ttl, zone FROM `"+rrtable+"` WHERE zone = ? AND (name = ? OR name = ?)";
121
122 if (!rrwhere.empty()) {
123 listQuery += " AND "+rrwhere;
124 basicQuery += " AND " + rrwhere;
125 anyQuery += " AND " + rrwhere;
126 }
127
128 d_listQuery_stmt = d_db->prepare(listQuery, 1);
129
130 anyQuery += ") UNION (SELECT 'SOA' AS type, origin AS data, '0' AS aux, ttl, id AS zone FROM `"+soatable+"` WHERE id = ? AND origin = ?";
131
132 if (!soawhere.empty()) {
133 anyQuery += " AND "+soawhere;
134 }
135
136 basicQuery += " ORDER BY type,aux,data";
137 anyQuery += ") ORDER BY type,aux,data";
138
139 d_basicQuery_stmt = d_db->prepare(basicQuery, 4);
140 d_anyQuery_stmt = d_db->prepare(anyQuery, 5);
141 } catch (SSqlException &e) {
142 L<<Logger::Error<<"Cannot prepare statements: " << e.txtReason() <<endl;
143 throw PDNSException("Cannot prepare statements: " + e.txtReason());
144 }
145 }
146
147 MyDNSBackend::~MyDNSBackend() {
148 delete d_domainIdQuery_stmt;
149 d_domainIdQuery_stmt = NULL;
150 delete d_domainNoIdQuery_stmt;
151 d_domainNoIdQuery_stmt = NULL;
152 delete d_listQuery_stmt;
153 d_listQuery_stmt = NULL;
154 delete d_soaQuery_stmt;
155 d_soaQuery_stmt = NULL;
156 delete d_basicQuery_stmt;
157 d_basicQuery_stmt = NULL;
158 delete d_anyQuery_stmt;
159 d_anyQuery_stmt = NULL;
160 delete(d_db);
161 }
162
163
164 bool MyDNSBackend::list(const DNSName &target, int zoneId, bool include_disabled) {
165 string query;
166 string sname;
167 SSqlStatement::row_t rrow;
168
169 try {
170 d_domainIdQuery_stmt->
171 bind("domain_id", zoneId)->
172 execute()->
173 getResult(d_result)->
174 reset();
175 }
176 catch (SSqlException &e) {
177 throw PDNSException("MyDNSBackend unable to list domain_id "+itoa(zoneId)+": "+e.txtReason());
178 }
179
180 if (d_result.empty())
181 return false; // No such zone
182
183 d_origin = d_result[0][0];
184 if (d_origin[d_origin.length()-1] == '.')
185 d_origin.erase(d_origin.length()-1);
186 d_minimum = pdns_stou(d_result[0][1]);
187
188 if (d_result.size()>1) {
189 L<<Logger::Warning<<backendName<<" Found more than one matching origin for zone ID: "<<zoneId<<endl;
190 };
191
192 try {
193 d_query_stmt = d_listQuery_stmt;
194 d_query_stmt->
195 bind("domain_id", zoneId)->
196 execute();
197 }
198 catch (SSqlException &e) {
199 throw PDNSException("MyDNSBackend unable to list domain_id "+itoa(zoneId)+": "+e.txtReason());
200 }
201
202 d_qname = "";
203 return true;
204 }
205
206 bool MyDNSBackend::getSOA(const DNSName& name, SOAData& soadata, DNSPacket*) {
207 string query;
208 SSqlStatement::row_t rrow;
209
210 if (name.empty())
211 return false;
212
213 try {
214 d_soaQuery_stmt->
215 bind("origin", name.toString())->
216 execute()->
217 getResult(d_result)->
218 reset();
219 }
220 catch (SSqlException &e) {
221 throw PDNSException("MyDNSBackend unable to get soa for domain "+name.toString()+": "+e.txtReason());
222 }
223
224 if (d_result.empty()) {
225 return false;
226 }
227
228 rrow = d_result[0];
229
230 soadata.qname = name;
231 soadata.domain_id = pdns_stou(rrow[0]);
232 soadata.hostmaster = DNSName(rrow[1]);
233 soadata.serial = pdns_stou(rrow[2]);
234 soadata.nameserver = DNSName(rrow[3]);
235 soadata.refresh = pdns_stou(rrow[4]);
236 soadata.retry = pdns_stou(rrow[5]);
237 soadata.expire = pdns_stou(rrow[6]);
238 soadata.default_ttl = pdns_stou(rrow[7]);
239 soadata.ttl = pdns_stou(rrow[8]);
240 if (d_useminimalttl) {
241 soadata.ttl = std::min(soadata.ttl, soadata.default_ttl);
242 }
243 soadata.db = this;
244
245 if (d_result.size()>1) {
246 L<<Logger::Warning<<backendName<<" Found more than one matching zone for: "<<name<<endl;
247 };
248
249 return true;
250 }
251
252 void MyDNSBackend::lookup(const QType &qtype, const DNSName &qname, DNSPacket *p, int zoneId) {
253 SSqlStatement::row_t rrow;
254 bool found = false;
255
256 DNSName sdom(qname);
257 d_origin = "";
258
259 if (qname.empty()) {
260 return;
261 }
262
263 DLOG(L<<Logger::Debug<<"MyDNSBackend::lookup(" << qtype.getName() << "," << qname << ",p," << zoneId << ")" << endl);
264
265 if (zoneId < 0) {
266 // First off we need to work out what zone we're working with
267 // MyDNS records aren't always fully qualified, so we need to work out the zone ID.
268
269
270 do {
271 try {
272 d_domainNoIdQuery_stmt->
273 bind("domain", sdom.toString())->
274 execute()->
275 getResult(d_result)->
276 reset();
277 }
278 catch (SSqlException &e) {
279 throw PDNSException("MyDNSBackend unable to lookup "+qname.toString()+": "+e.txtReason());
280 }
281
282 if (d_result.empty() == false) {
283 rrow = d_result[0];
284 zoneId = pdns_stou(rrow[0]);
285 d_origin = stripDot(rrow[1]);
286 d_minimum = pdns_stou(rrow[2]);
287 found = true;
288 break;
289 }
290
291 } while(sdom.chopOff());
292
293 } else {
294 try {
295 d_domainIdQuery_stmt->
296 bind("domain_id", zoneId)->
297 execute()->
298 getResult(d_result)->
299 reset();
300 }
301 catch (SSqlException &e) {
302 throw PDNSException("MyDNSBackend unable to lookup "+qname.toString()+": "+e.txtReason());
303 }
304
305 if(d_result.empty()) {
306 throw PDNSException("lookup() passed zoneId = "+itoa(zoneId)+" but no such zone!");
307 }
308
309 rrow = d_result[0];
310
311 found = true;
312 d_origin = stripDot(rrow[0]);
313 d_minimum = pdns_stou(rrow[1]);
314 }
315
316 if (found) {
317
318 while (d_result.size()>1) {
319 L<<Logger::Warning<<backendName<<" Found more than one matching zone for: "+d_origin<<endl;
320 };
321 // We found the zoneId, so we can work out how to find our rr
322 string host;
323
324 // The host part of the query is the name less the origin
325 DNSName origin(d_origin);
326 host = qname.makeRelative(origin).toStringNoDot();
327
328 try {
329
330 if (qtype.getCode()==QType::ANY) {
331 DLOG(L<<Logger::Debug<<"Running d_anyQuery_stmt with " << zoneId << ", " << host << ", " << sdom << ", " << zoneId <<" , "<< qname << ", " << qtype.getName() << endl);
332 d_query_stmt = d_anyQuery_stmt;
333 d_query_stmt->
334 bind("domain_id", zoneId)->
335 bind("host", host)->
336 bind("qname", qname.toString())->
337 bind("domain_id", zoneId)-> // this is because positional arguments
338 bind("qname2", sdom.toString())->
339 execute();
340 } else {
341 DLOG(L<<Logger::Debug<<"Running d_basicQuery_stmt with " << zoneId << ", " << host << ", " << qname << ", " << qtype.getName() << endl);
342 d_query_stmt = d_basicQuery_stmt;
343 d_query_stmt->
344 bind("domain_id", zoneId)->
345 bind("host", host)->
346 bind("qname", qname.toString())->
347 bind("qtype", qtype.getName())->
348 execute();
349 }
350 }
351 catch (SSqlException &e) {
352 throw PDNSException("MyDNSBackend unable to lookup "+qname.toString()+": "+e.txtReason());
353 }
354
355 d_qname = qname.toString();
356 }
357
358 }
359
360 bool MyDNSBackend::get(DNSResourceRecord &rr) {
361 if (d_origin.empty()) {
362 if (d_query_stmt) {
363 try {
364 d_query_stmt->reset();
365 } catch (SSqlException &e) {
366 throw PDNSException("MyDNSBackend unable to lookup "+d_qname+": "+e.txtReason());
367 }
368 d_query_stmt = NULL;
369 }
370 // This happens if lookup() couldn't find the zone
371 return false;
372 }
373
374 SSqlStatement::row_t rrow;
375
376 if (d_query_stmt->hasNextRow()) {
377 try {
378 d_query_stmt->nextRow(rrow);
379 } catch (SSqlException &e) {
380 throw PDNSException("MyDNSBackend unable to lookup "+d_qname+": "+e.txtReason());
381 }
382 rr.qtype=rrow[0];
383 rr.content = rrow[1];
384
385 if(!d_qname.empty()) {
386 // use this to distinguish between select with 'name' field (list()) and one without
387 rr.qname=DNSName(d_qname);
388 } else {
389 string tmpQname = rrow[5];
390
391 //TODO: Refactor
392 if (!tmpQname.empty() && tmpQname[tmpQname.length()-1] == '.') {
393 tmpQname.erase(tmpQname.length()-1); // Fully qualified, nuke the last .
394 } else {
395 if (!tmpQname.empty()) {
396 tmpQname += ".";
397 }
398 tmpQname += d_origin; // Not fully qualified
399 }
400 rr.qname = DNSName(tmpQname);
401 }
402
403 if (rr.qtype.getCode() == QType::NS || rr.qtype.getCode()==QType::MX ||
404 rr.qtype.getCode() == QType::CNAME || rr.qtype.getCode() == QType::PTR) {
405 if (!rr.content.empty() && rr.content[rr.content.length()-1] == '.') {
406 if (rr.content.length() > 1)
407 rr.content.erase(rr.content.length()-1); // Fully qualified, nuke the last .
408 } else {
409 if (rr.content != ".")
410 rr.content += ".";
411 rr.content += d_origin;
412 }
413 }
414
415 if (rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV)
416 rr.content=rrow[2]+" "+rr.content;
417
418 rr.ttl = pdns_stou(rrow[3]);
419 if (d_useminimalttl)
420 rr.ttl = std::min(rr.ttl, d_minimum);
421 rr.domain_id=pdns_stou(rrow[4]);
422
423 rr.last_modified=0;
424
425 return true;
426 }
427
428 try {
429 d_query_stmt->reset();
430 } catch (SSqlException &e) {
431 throw PDNSException("MyDNSBackend unable to lookup "+d_qname+": "+e.txtReason());
432 }
433
434 d_query_stmt = NULL;
435
436 return false;
437 }
438
439 class MyDNSFactory : public BackendFactory {
440
441 public:
442 MyDNSFactory() : BackendFactory("mydns") {}
443
444 void declareArguments(const string &suffix = "") {
445 declare(suffix,"dbname","Pdns backend database name to connect to","mydns");
446 declare(suffix,"user","Pdns backend user to connect as","powerdns");
447 declare(suffix,"host","Pdns backend host to connect to","");
448 declare(suffix,"port","Pdns backend host to connect to","");
449 declare(suffix,"password","Pdns backend password to connect with","");
450 declare(suffix,"socket","Pdns backend socket to connect to","");
451 declare(suffix,"rr-table","Name of RR table to use","rr");
452 declare(suffix,"soa-table","Name of SOA table to use","soa");
453 declare(suffix,"soa-where","Additional WHERE clause for SOA","1 = 1");
454 declare(suffix,"rr-where","Additional WHERE clause for RR","1 = 1");
455 declare(suffix,"soa-active","Use the active column in the SOA table","yes");
456 declare(suffix,"rr-active","Use the active column in the RR table","yes");
457 declare(suffix,"use-minimal-ttl","Setting this to 'yes' will make the backend behave like MyDNS on the TTL values. Setting it to 'no' will make it ignore the minimal-ttl of the zone.","yes");
458 }
459
460 DNSBackend *make(const string &suffix="") {
461 return new MyDNSBackend(suffix);
462 }
463
464 };
465
466 class MyDNSLoader {
467
468 public:
469 MyDNSLoader() {
470 BackendMakers().report(new MyDNSFactory());
471 L << Logger::Info << "[mydnsbackend] This is the mydns backend version " VERSION
472 #ifndef REPRODUCIBLE
473 << " (" __DATE__ " " __TIME__ ")"
474 #endif
475 << " reporting" << endl;
476 }
477 };
478
479 static MyDNSLoader mydnsloader;