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