]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/ws-auth.cc
Merge pull request #7503 from ahupowerdns/auth-ringbugs
[thirdparty/pdns.git] / pdns / ws-auth.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 #include "utility.hh"
26 #include "dynlistener.hh"
27 #include "ws-auth.hh"
28 #include "json.hh"
29 #include "webserver.hh"
30 #include "logger.hh"
31 #include "statbag.hh"
32 #include "misc.hh"
33 #include "base64.hh"
34 #include "arguments.hh"
35 #include "dns.hh"
36 #include "comment.hh"
37 #include "ueberbackend.hh"
38 #include <boost/format.hpp>
39
40 #include "namespaces.hh"
41 #include "ws-api.hh"
42 #include "version.hh"
43 #include "dnsseckeeper.hh"
44 #include <iomanip>
45 #include "zoneparser-tng.hh"
46 #include "common_startup.hh"
47 #include "auth-caches.hh"
48 #include "threadname.hh"
49 #include "tsigutils.hh"
50
51 using json11::Json;
52
53 extern StatBag S;
54
55 static void patchZone(HttpRequest* req, HttpResponse* resp);
56 static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs);
57 static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
58
59 // QTypes that MUST NOT have multiple records of the same type in a given RRset.
60 static const std::set<uint16_t> onlyOneEntryTypes = { QType::CNAME, QType::DNAME, QType::SOA };
61 // QTypes that MUST NOT be used with any other QType on the same name.
62 static const std::set<uint16_t> exclusiveEntryTypes = { QType::CNAME, QType::DNAME };
63
64 AuthWebServer::AuthWebServer() :
65 d_tid(0),
66 d_start(time(nullptr)),
67 d_min10(0),
68 d_min5(0),
69 d_min1(0)
70 {
71 if(arg().mustDo("webserver") || arg().mustDo("api")) {
72 d_ws = new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
73 d_ws->setApiKey(arg()["api-key"]);
74 d_ws->setPassword(arg()["webserver-password"]);
75
76 NetmaskGroup acl;
77 acl.toMasks(::arg()["webserver-allow-from"]);
78 d_ws->setACL(acl);
79
80 d_ws->bind();
81 }
82 }
83
84 void AuthWebServer::go()
85 {
86 S.doRings();
87 pthread_create(&d_tid, 0, webThreadHelper, this);
88 pthread_create(&d_tid, 0, statThreadHelper, this);
89 }
90
91 void AuthWebServer::statThread()
92 {
93 try {
94 setThreadName("pdns/statHelper");
95 for(;;) {
96 d_queries.submit(S.read("udp-queries"));
97 d_cachehits.submit(S.read("packetcache-hit"));
98 d_cachemisses.submit(S.read("packetcache-miss"));
99 d_qcachehits.submit(S.read("query-cache-hit"));
100 d_qcachemisses.submit(S.read("query-cache-miss"));
101 Utility::sleep(1);
102 }
103 }
104 catch(...) {
105 g_log<<Logger::Error<<"Webserver statThread caught an exception, dying"<<endl;
106 _exit(1);
107 }
108 }
109
110 void *AuthWebServer::statThreadHelper(void *p)
111 {
112 AuthWebServer *self=static_cast<AuthWebServer *>(p);
113 self->statThread();
114 return 0; // never reached
115 }
116
117 void *AuthWebServer::webThreadHelper(void *p)
118 {
119 AuthWebServer *self=static_cast<AuthWebServer *>(p);
120 self->webThread();
121 return 0; // never reached
122 }
123
124 static string htmlescape(const string &s) {
125 string result;
126 for(string::const_iterator it=s.begin(); it!=s.end(); ++it) {
127 switch (*it) {
128 case '&':
129 result += "&amp;";
130 break;
131 case '<':
132 result += "&lt;";
133 break;
134 case '>':
135 result += "&gt;";
136 break;
137 case '"':
138 result += "&quot;";
139 break;
140 default:
141 result += *it;
142 }
143 }
144 return result;
145 }
146
147 void printtable(ostringstream &ret, const string &ringname, const string &title, int limit=10)
148 {
149 int tot=0;
150 int entries=0;
151 vector<pair <string,unsigned int> >ring=S.getRing(ringname);
152
153 for(vector<pair<string, unsigned int> >::const_iterator i=ring.begin(); i!=ring.end();++i) {
154 tot+=i->second;
155 entries++;
156 }
157
158 ret<<"<div class=\"panel\">";
159 ret<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname)<<"\">Reset</a></span>"<<endl;
160 ret<<"<h2>"<<title<<"</h2>"<<endl;
161 ret<<"<div class=ringmeta>";
162 ret<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname)<<"\">Showing: Top "<<limit<<" of "<<entries<<"</a>"<<endl;
163 ret<<"<span class=resizering>Resize: ";
164 unsigned int sizes[]={10,100,500,1000,10000,500000,0};
165 for(int i=0;sizes[i];++i) {
166 if(S.getRingSize(ringname)!=sizes[i])
167 ret<<"<a href=\"?resizering="<<htmlescape(ringname)<<"&amp;size="<<sizes[i]<<"\">"<<sizes[i]<<"</a> ";
168 else
169 ret<<"("<<sizes[i]<<") ";
170 }
171 ret<<"</span></div>";
172
173 ret<<"<table class=\"data\">";
174 int printed=0;
175 int total=max(1,tot);
176 for(vector<pair<string,unsigned int> >::const_iterator i=ring.begin();limit && i!=ring.end();++i,--limit) {
177 ret<<"<tr><td>"<<htmlescape(i->first)<<"</td><td>"<<i->second<<"</td><td align=right>"<< AuthWebServer::makePercentage(i->second*100.0/total)<<"</td>"<<endl;
178 printed+=i->second;
179 }
180 ret<<"<tr><td colspan=3></td></tr>"<<endl;
181 if(printed!=tot)
182 ret<<"<tr><td><b>Rest:</b></td><td><b>"<<tot-printed<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot-printed)*100.0/total)<<"</b></td>"<<endl;
183
184 ret<<"<tr><td><b>Total:</b></td><td><b>"<<tot<<"</b></td><td align=right><b>100%</b></td>";
185 ret<<"</table></div>"<<endl;
186 }
187
188 void AuthWebServer::printvars(ostringstream &ret)
189 {
190 ret<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl;
191
192 vector<string>entries=S.getEntries();
193 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
194 ret<<"<tr><td>"<<*i<<"</td><td>"<<S.read(*i)<<"</td><td>"<<S.getDescrip(*i)<<"</td>"<<endl;
195 }
196
197 ret<<"</table></div>"<<endl;
198 }
199
200 void AuthWebServer::printargs(ostringstream &ret)
201 {
202 ret<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl;
203
204 vector<string>entries=arg().list();
205 for(vector<string>::const_iterator i=entries.begin();i!=entries.end();++i) {
206 ret<<"<tr><td>"<<*i<<"</td><td>"<<arg()[*i]<<"</td><td>"<<arg().getHelp(*i)<<"</td>"<<endl;
207 }
208 }
209
210 string AuthWebServer::makePercentage(const double& val)
211 {
212 return (boost::format("%.01f%%") % val).str();
213 }
214
215 void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
216 {
217 if(!req->getvars["resetring"].empty()) {
218 if (S.ringExists(req->getvars["resetring"]))
219 S.resetRing(req->getvars["resetring"]);
220 resp->status = 302;
221 resp->headers["Location"] = req->url.path;
222 return;
223 }
224 if(!req->getvars["resizering"].empty()){
225 int size=std::stoi(req->getvars["size"]);
226 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000)
227 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
228 resp->status = 302;
229 resp->headers["Location"] = req->url.path;
230 return;
231 }
232
233 ostringstream ret;
234
235 ret<<"<!DOCTYPE html>"<<endl;
236 ret<<"<html><head>"<<endl;
237 ret<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl;
238 ret<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl;
239 ret<<"</head><body>"<<endl;
240
241 ret<<"<div class=\"row\">"<<endl;
242 ret<<"<div class=\"headl columns\">";
243 ret<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION);
244 if(!arg()["config-name"].empty()) {
245 ret<<" ["<<htmlescape(arg()["config-name"])<<"]";
246 }
247 ret<<"</a></div>"<<endl;
248 ret<<"<div class=\"headr columns\"></div></div>";
249 ret<<"<div class=\"row\"><div class=\"all columns\">";
250
251 time_t passed=time(0)-s_starttime;
252
253 ret<<"<p>Uptime: "<<
254 humanDuration(passed)<<
255 "<br>"<<endl;
256
257 ret<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
258 (int)d_queries.get1()<<", "<<
259 (int)d_queries.get5()<<", "<<
260 (int)d_queries.get10()<<". Max queries/second: "<<(int)d_queries.getMax()<<
261 "<br>"<<endl;
262
263 if(d_cachemisses.get10()+d_cachehits.get10()>0)
264 ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
265 makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
266 makePercentage((d_cachehits.get5()*100.0)/((d_cachehits.get5())+(d_cachemisses.get5())))<<", "<<
267 makePercentage((d_cachehits.get10()*100.0)/((d_cachehits.get10())+(d_cachemisses.get10())))<<
268 "<br>"<<endl;
269
270 if(d_qcachemisses.get10()+d_qcachehits.get10()>0)
271 ret<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
272 makePercentage((d_qcachehits.get1()*100.0)/((d_qcachehits.get1())+(d_qcachemisses.get1())))<<", "<<
273 makePercentage((d_qcachehits.get5()*100.0)/((d_qcachehits.get5())+(d_qcachemisses.get5())))<<", "<<
274 makePercentage((d_qcachehits.get10()*100.0)/((d_qcachehits.get10())+(d_qcachemisses.get10())))<<
275 "<br>"<<endl;
276
277 ret<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
278 (int)d_qcachemisses.get1()<<", "<<
279 (int)d_qcachemisses.get5()<<", "<<
280 (int)d_qcachemisses.get10()<<". Max queries/second: "<<(int)d_qcachemisses.getMax()<<
281 "<br>"<<endl;
282
283 ret<<"Total queries: "<<S.read("udp-queries")<<". Question/answer latency: "<<S.read("latency")/1000.0<<"ms</p><br>"<<endl;
284 if(req->getvars["ring"].empty()) {
285 auto entries = S.listRings();
286 for(const auto &i: entries) {
287 printtable(ret, i, S.getRingTitle(i));
288 }
289
290 printvars(ret);
291 if(arg().mustDo("webserver-print-arguments"))
292 printargs(ret);
293 }
294 else if(S.ringExists(req->getvars["ring"]))
295 printtable(ret,req->getvars["ring"],S.getRingTitle(req->getvars["ring"]),100);
296
297 ret<<"</div></div>"<<endl;
298 ret<<"<footer class=\"row\">"<<fullVersionString()<<"<br>&copy; 2013 - 2019 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl;
299 ret<<"</body></html>"<<endl;
300
301 resp->body = ret.str();
302 resp->status = 200;
303 }
304
305 /** Helper to build a record content as needed. */
306 static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
307 // noDot: for backend storage, pass true. for API users, pass false.
308 auto drc = DNSRecordContent::mastermake(qtype.getCode(), QClass::IN, content);
309 return drc->getZoneRepresentation(noDot);
310 }
311
312 /** "Normalize" record content for API consumers. */
313 static inline string makeApiRecordContent(const QType& qtype, const string& content) {
314 return makeRecordContent(qtype, content, false);
315 }
316
317 /** "Normalize" record content for backend storage. */
318 static inline string makeBackendRecordContent(const QType& qtype, const string& content) {
319 return makeRecordContent(qtype, content, true);
320 }
321
322 static Json::object getZoneInfo(const DomainInfo& di, DNSSECKeeper *dk) {
323 string zoneId = apiZoneNameToId(di.zone);
324 vector<string> masters;
325 for(const auto& m : di.masters)
326 masters.push_back(m.toStringWithPortExcept(53));
327
328 return Json::object {
329 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
330 { "id", zoneId },
331 { "url", "/api/v1/servers/localhost/zones/" + zoneId },
332 { "name", di.zone.toString() },
333 { "kind", di.getKindString() },
334 { "dnssec", dk->isSecuredZone(di.zone) },
335 { "account", di.account },
336 { "masters", masters },
337 { "serial", (double)di.serial },
338 { "notified_serial", (double)di.notified_serial },
339 { "last_check", (double)di.last_check }
340 };
341 }
342
343 static bool shouldDoRRSets(HttpRequest* req) {
344 if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true")
345 return true;
346 if (req->getvars["rrsets"] == "false")
347 return false;
348 throw ApiException("'rrsets' request parameter value '"+req->getvars["rrsets"]+"' is not supported");
349 }
350
351 static void fillZone(const DNSName& zonename, HttpResponse* resp, bool doRRSets) {
352 UeberBackend B;
353 DomainInfo di;
354 if(!B.getDomainInfo(zonename, di)) {
355 throw HttpNotFoundException();
356 }
357
358 DNSSECKeeper dk(&B);
359 Json::object doc = getZoneInfo(di, &dk);
360 // extra stuff getZoneInfo doesn't do for us (more expensive)
361 string soa_edit_api;
362 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
363 doc["soa_edit_api"] = soa_edit_api;
364 string soa_edit;
365 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
366 doc["soa_edit"] = soa_edit;
367 string nsec3param;
368 di.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
369 doc["nsec3param"] = nsec3param;
370 string nsec3narrow;
371 bool nsec3narrowbool = false;
372 di.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
373 if (nsec3narrow == "1")
374 nsec3narrowbool = true;
375 doc["nsec3narrow"] = nsec3narrowbool;
376
377 string api_rectify;
378 di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
379 doc["api_rectify"] = (api_rectify == "1");
380
381 // TSIG
382 vector<string> tsig_master, tsig_slave;
383 di.backend->getDomainMetadata(zonename, "TSIG-ALLOW-AXFR", tsig_master);
384 di.backend->getDomainMetadata(zonename, "AXFR-MASTER-TSIG", tsig_slave);
385
386 Json::array tsig_master_keys;
387 for (const auto& keyname : tsig_master) {
388 tsig_master_keys.push_back(apiZoneNameToId(DNSName(keyname)));
389 }
390 doc["master_tsig_key_ids"] = tsig_master_keys;
391
392 Json::array tsig_slave_keys;
393 for (const auto& keyname : tsig_slave) {
394 tsig_slave_keys.push_back(apiZoneNameToId(DNSName(keyname)));
395 }
396 doc["slave_tsig_key_ids"] = tsig_slave_keys;
397
398 if (doRRSets) {
399 vector<DNSResourceRecord> records;
400 vector<Comment> comments;
401
402 // load all records + sort
403 {
404 DNSResourceRecord rr;
405 di.backend->list(zonename, di.id, true); // incl. disabled
406 while(di.backend->get(rr)) {
407 if (!rr.qtype.getCode())
408 continue; // skip empty non-terminals
409 records.push_back(rr);
410 }
411 sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
412 /* if you ever want to update this comparison function,
413 please be aware that you will also need to update the conditions in the code merging
414 the records and comments below */
415 if (a.qname == b.qname) {
416 return b.qtype < a.qtype;
417 }
418 return b.qname < a.qname;
419 });
420 }
421
422 // load all comments + sort
423 {
424 Comment comment;
425 di.backend->listComments(di.id);
426 while(di.backend->getComment(comment)) {
427 comments.push_back(comment);
428 }
429 sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
430 /* if you ever want to update this comparison function,
431 please be aware that you will also need to update the conditions in the code merging
432 the records and comments below */
433 if (a.qname == b.qname) {
434 return b.qtype < a.qtype;
435 }
436 return b.qname < a.qname;
437 });
438 }
439
440 Json::array rrsets;
441 Json::object rrset;
442 Json::array rrset_records;
443 Json::array rrset_comments;
444 DNSName current_qname;
445 QType current_qtype;
446 uint32_t ttl;
447 auto rit = records.begin();
448 auto cit = comments.begin();
449
450 while (rit != records.end() || cit != comments.end()) {
451 // if you think this should be rit < cit instead of cit < rit, note the b < a instead of a < b in the sort comparison functions above
452 if (cit == comments.end() || (rit != records.end() && (rit->qname == cit->qname ? (cit->qtype < rit->qtype || cit->qtype == rit->qtype) : cit->qname < rit->qname))) {
453 current_qname = rit->qname;
454 current_qtype = rit->qtype;
455 ttl = rit->ttl;
456 } else {
457 current_qname = cit->qname;
458 current_qtype = cit->qtype;
459 ttl = 0;
460 }
461
462 while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
463 ttl = min(ttl, rit->ttl);
464 rrset_records.push_back(Json::object {
465 { "disabled", rit->disabled },
466 { "content", makeApiRecordContent(rit->qtype, rit->content) }
467 });
468 rit++;
469 }
470 while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
471 rrset_comments.push_back(Json::object {
472 { "modified_at", (double)cit->modified_at },
473 { "account", cit->account },
474 { "content", cit->content }
475 });
476 cit++;
477 }
478
479 rrset["name"] = current_qname.toString();
480 rrset["type"] = current_qtype.getName();
481 rrset["records"] = rrset_records;
482 rrset["comments"] = rrset_comments;
483 rrset["ttl"] = (double)ttl;
484 rrsets.push_back(rrset);
485 rrset.clear();
486 rrset_records.clear();
487 rrset_comments.clear();
488 }
489
490 doc["rrsets"] = rrsets;
491 }
492
493 resp->setBody(doc);
494 }
495
496 void productServerStatisticsFetch(map<string,string>& out)
497 {
498 vector<string> items = S.getEntries();
499 for(const string& item : items) {
500 out[item] = std::to_string(S.read(item));
501 }
502
503 // add uptime
504 out["uptime"] = std::to_string(time(0) - s_starttime);
505 }
506
507 boost::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
508 {
509 try {
510 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
511 return S.read(name);
512 }
513 catch(...) {
514 return boost::none;
515 }
516 }
517
518 static void validateGatheredRRType(const DNSResourceRecord& rr) {
519 if (rr.qtype.getCode() == QType::OPT || rr.qtype.getCode() == QType::TSIG) {
520 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": invalid type given");
521 }
522 }
523
524 static void gatherRecords(const Json container, const DNSName& qname, const QType qtype, const int ttl, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
525 UeberBackend B;
526 DNSResourceRecord rr;
527 rr.qname = qname;
528 rr.qtype = qtype;
529 rr.auth = 1;
530 rr.ttl = ttl;
531
532 validateGatheredRRType(rr);
533 const auto& items = container["records"].array_items();
534 for(const auto& record : items) {
535 string content = stringFromJson(record, "content");
536 rr.disabled = boolFromJson(record, "disabled");
537
538 // validate that the client sent something we can actually parse, and require that data to be dotted.
539 try {
540 if (rr.qtype.getCode() != QType::AAAA) {
541 string tmp = makeApiRecordContent(rr.qtype, content);
542 if (!pdns_iequals(tmp, content)) {
543 throw std::runtime_error("Not in expected format (parsed as '"+tmp+"')");
544 }
545 } else {
546 struct in6_addr tmpbuf;
547 if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
548 throw std::runtime_error("Invalid IPv6 address");
549 }
550 }
551 rr.content = makeBackendRecordContent(rr.qtype, content);
552 }
553 catch(std::exception& e)
554 {
555 throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" '"+content+"': "+e.what());
556 }
557
558 if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
559 boolFromJson(record, "set-ptr", false) == true) {
560 DNSResourceRecord ptr;
561 makePtr(rr, &ptr);
562
563 // verify that there's a zone for the PTR
564 SOAData sd;
565 if (!B.getAuth(ptr.qname, QType(QType::PTR), &sd, false))
566 throw ApiException("Could not find domain for PTR '"+ptr.qname.toString()+"' requested for '"+ptr.content+"'");
567
568 ptr.domain_id = sd.domain_id;
569 new_ptrs.push_back(ptr);
570 }
571
572 new_records.push_back(rr);
573 }
574 }
575
576 static void gatherComments(const Json container, const DNSName& qname, const QType qtype, vector<Comment>& new_comments) {
577 Comment c;
578 c.qname = qname;
579 c.qtype = qtype;
580
581 time_t now = time(0);
582 for (auto comment : container["comments"].array_items()) {
583 c.modified_at = intFromJson(comment, "modified_at", now);
584 c.content = stringFromJson(comment, "content");
585 c.account = stringFromJson(comment, "account");
586 new_comments.push_back(c);
587 }
588 }
589
590 static void checkDefaultDNSSECAlgos() {
591 int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
592 int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
593 int k_size = arg().asNum("default-ksk-size");
594 int z_size = arg().asNum("default-zsk-size");
595
596 // Sanity check DNSSEC parameters
597 if (::arg()["default-zsk-algorithm"] != "") {
598 if (k_algo == -1)
599 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
600 else if (k_algo <= 10 && k_size == 0)
601 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
602 }
603
604 if (::arg()["default-zsk-algorithm"] != "") {
605 if (z_algo == -1)
606 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
607 else if (z_algo <= 10 && z_size == 0)
608 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
609 }
610 }
611
612 static void throwUnableToSecure(const DNSName& zonename) {
613 throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
614 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
615 }
616
617 static void updateDomainSettingsFromDocument(UeberBackend& B, const DomainInfo& di, const DNSName& zonename, const Json document) {
618 vector<string> zonemaster;
619 bool shouldRectify = false;
620 for(auto value : document["masters"].array_items()) {
621 string master = value.string_value();
622 if (master.empty())
623 throw ApiException("Master can not be an empty string");
624 zonemaster.push_back(master);
625 }
626
627 if (zonemaster.size()) {
628 di.backend->setMaster(zonename, boost::join(zonemaster, ","));
629 }
630 if (document["kind"].is_string()) {
631 di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
632 }
633 if (document["soa_edit_api"].is_string()) {
634 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
635 }
636 if (document["soa_edit"].is_string()) {
637 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
638 }
639 try {
640 bool api_rectify = boolFromJson(document, "api_rectify");
641 di.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0");
642 }
643 catch (const JsonException&) {}
644
645 if (document["account"].is_string()) {
646 di.backend->setAccount(zonename, document["account"].string_value());
647 }
648
649 DNSSECKeeper dk(&B);
650 bool dnssecInJSON = false;
651 bool dnssecDocVal = false;
652
653 try {
654 dnssecDocVal = boolFromJson(document, "dnssec");
655 dnssecInJSON = true;
656 }
657 catch (const JsonException&) {}
658
659 bool isDNSSECZone = dk.isSecuredZone(zonename);
660
661 if (dnssecInJSON) {
662 if (dnssecDocVal) {
663 if (!isDNSSECZone) {
664 checkDefaultDNSSECAlgos();
665
666 int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
667 int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
668 int k_size = arg().asNum("default-ksk-size");
669 int z_size = arg().asNum("default-zsk-size");
670
671 if (k_algo != -1) {
672 int64_t id;
673 if (!dk.addKey(zonename, true, k_algo, id, k_size)) {
674 throwUnableToSecure(zonename);
675 }
676 }
677
678 if (z_algo != -1) {
679 int64_t id;
680 if (!dk.addKey(zonename, false, z_algo, id, z_size)) {
681 throwUnableToSecure(zonename);
682 }
683 }
684
685 // Used later for NSEC3PARAM
686 isDNSSECZone = dk.isSecuredZone(zonename);
687
688 if (!isDNSSECZone) {
689 throwUnableToSecure(zonename);
690 }
691 shouldRectify = true;
692 }
693 } else {
694 // "dnssec": false in json
695 if (isDNSSECZone) {
696 string info, error;
697 if (!dk.unSecureZone(zonename, error, info)) {
698 throw ApiException("Error while un-securing zone '"+ zonename.toString()+"': " + error);
699 }
700 isDNSSECZone = dk.isSecuredZone(zonename);
701 if (isDNSSECZone) {
702 throw ApiException("Unable to un-secure zone '"+ zonename.toString()+"'");
703 }
704 shouldRectify = true;
705 }
706 }
707 }
708
709 if(document["nsec3param"].string_value().length() > 0) {
710 shouldRectify = true;
711 NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
712 string error_msg = "";
713 if (!isDNSSECZone) {
714 throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"', but zone is not DNSSEC secured.");
715 }
716 if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
717 throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
718 }
719 if (!dk.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
720 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() +
721 "' passed our basic sanity checks, but cannot be used with the current backend.");
722 }
723 }
724
725 if (shouldRectify && !dk.isPresigned(zonename)) {
726 // Rectify
727 string api_rectify;
728 di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
729 if (api_rectify.empty()) {
730 if (::arg().mustDo("default-api-rectify")) {
731 api_rectify = "1";
732 }
733 }
734 if (api_rectify == "1") {
735 string info;
736 string error_msg;
737 if (!dk.rectifyZone(zonename, error_msg, info, true)) {
738 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
739 }
740 }
741
742 // Increase serial
743 string soa_edit_api_kind;
744 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
745 if (!soa_edit_api_kind.empty()) {
746 SOAData sd;
747 if (!B.getSOAUncached(zonename, sd))
748 return;
749
750 string soa_edit_kind;
751 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
752
753 DNSResourceRecord rr;
754 if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
755 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
756 throw ApiException("Hosting backend does not support editing records.");
757 }
758 }
759 }
760 }
761
762 if (!document["master_tsig_key_ids"].is_null()) {
763 vector<string> metadata;
764 DNSName keyAlgo;
765 string keyContent;
766 for(auto value : document["master_tsig_key_ids"].array_items()) {
767 auto keyname(apiZoneIdToName(value.string_value()));
768 B.getTSIGKey(keyname, &keyAlgo, &keyContent);
769 if (keyAlgo.empty() || keyContent.empty()) {
770 throw ApiException("A TSIG key with the name '"+keyname.toLogString()+"' does not exist");
771 }
772 metadata.push_back(keyname.toString());
773 }
774 if (!di.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) {
775 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename.toLogString() + "'");
776 }
777 }
778 if (!document["slave_tsig_key_ids"].is_null()) {
779 vector<string> metadata;
780 DNSName keyAlgo;
781 string keyContent;
782 for(auto value : document["slave_tsig_key_ids"].array_items()) {
783 auto keyname(apiZoneIdToName(value.string_value()));
784 B.getTSIGKey(keyname, &keyAlgo, &keyContent);
785 if (keyAlgo.empty() || keyContent.empty()) {
786 throw ApiException("A TSIG key with the name '"+keyname.toLogString()+"' does not exist");
787 }
788 metadata.push_back(keyname.toString());
789 }
790 if (!di.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) {
791 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename.toLogString() + "'");
792 }
793 }
794 }
795
796 static bool isValidMetadataKind(const string& kind, bool readonly) {
797 static vector<string> builtinOptions {
798 "ALLOW-AXFR-FROM",
799 "AXFR-SOURCE",
800 "ALLOW-DNSUPDATE-FROM",
801 "TSIG-ALLOW-DNSUPDATE",
802 "FORWARD-DNSUPDATE",
803 "SOA-EDIT-DNSUPDATE",
804 "NOTIFY-DNSUPDATE",
805 "ALSO-NOTIFY",
806 "AXFR-MASTER-TSIG",
807 "GSS-ALLOW-AXFR-PRINCIPAL",
808 "GSS-ACCEPTOR-PRINCIPAL",
809 "IXFR",
810 "LUA-AXFR-SCRIPT",
811 "NSEC3NARROW",
812 "NSEC3PARAM",
813 "PRESIGNED",
814 "PUBLISH-CDNSKEY",
815 "PUBLISH-CDS",
816 "SOA-EDIT",
817 "TSIG-ALLOW-AXFR",
818 "TSIG-ALLOW-DNSUPDATE"
819 };
820
821 // the following options do not allow modifications via API
822 static vector<string> protectedOptions {
823 "API-RECTIFY",
824 "AXFR-MASTER-TSIG",
825 "NSEC3NARROW",
826 "NSEC3PARAM",
827 "PRESIGNED",
828 "LUA-AXFR-SCRIPT",
829 "TSIG-ALLOW-AXFR"
830 };
831
832 if (kind.find("X-") == 0)
833 return true;
834
835 bool found = false;
836
837 for (const string& s : builtinOptions) {
838 if (kind == s) {
839 for (const string& s2 : protectedOptions) {
840 if (!readonly && s == s2)
841 return false;
842 }
843 found = true;
844 break;
845 }
846 }
847
848 return found;
849 }
850
851 static void apiZoneMetadata(HttpRequest* req, HttpResponse *resp) {
852 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
853
854 UeberBackend B;
855 DomainInfo di;
856 if (!B.getDomainInfo(zonename, di)) {
857 throw HttpNotFoundException();
858 }
859
860 if (req->method == "GET") {
861 map<string, vector<string> > md;
862 Json::array document;
863
864 if (!B.getAllDomainMetadata(zonename, md))
865 throw HttpNotFoundException();
866
867 for (const auto& i : md) {
868 Json::array entries;
869 for (string j : i.second)
870 entries.push_back(j);
871
872 Json::object key {
873 { "type", "Metadata" },
874 { "kind", i.first },
875 { "metadata", entries }
876 };
877
878 document.push_back(key);
879 }
880
881 resp->setBody(document);
882 } else if (req->method == "POST") {
883 auto document = req->json();
884 string kind;
885 vector<string> entries;
886
887 try {
888 kind = stringFromJson(document, "kind");
889 } catch (const JsonException&) {
890 throw ApiException("kind is not specified or not a string");
891 }
892
893 if (!isValidMetadataKind(kind, false))
894 throw ApiException("Unsupported metadata kind '" + kind + "'");
895
896 vector<string> vecMetadata;
897
898 if (!B.getDomainMetadata(zonename, kind, vecMetadata))
899 throw ApiException("Could not retrieve metadata entries for domain '" +
900 zonename.toString() + "'");
901
902 auto& metadata = document["metadata"];
903 if (!metadata.is_array())
904 throw ApiException("metadata is not specified or not an array");
905
906 for (const auto& i : metadata.array_items()) {
907 if (!i.is_string())
908 throw ApiException("metadata must be strings");
909 else if (std::find(vecMetadata.cbegin(),
910 vecMetadata.cend(),
911 i.string_value()) == vecMetadata.cend()) {
912 vecMetadata.push_back(i.string_value());
913 }
914 }
915
916 if (!B.setDomainMetadata(zonename, kind, vecMetadata))
917 throw ApiException("Could not update metadata entries for domain '" +
918 zonename.toString() + "'");
919
920 Json::array respMetadata;
921 for (const string& s : vecMetadata)
922 respMetadata.push_back(s);
923
924 Json::object key {
925 { "type", "Metadata" },
926 { "kind", document["kind"] },
927 { "metadata", respMetadata }
928 };
929
930 resp->status = 201;
931 resp->setBody(key);
932 } else
933 throw HttpMethodNotAllowedException();
934 }
935
936 static void apiZoneMetadataKind(HttpRequest* req, HttpResponse* resp) {
937 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
938
939 UeberBackend B;
940 DomainInfo di;
941 if (!B.getDomainInfo(zonename, di)) {
942 throw HttpNotFoundException();
943 }
944
945 string kind = req->parameters["kind"];
946
947 if (req->method == "GET") {
948 vector<string> metadata;
949 Json::object document;
950 Json::array entries;
951
952 if (!B.getDomainMetadata(zonename, kind, metadata))
953 throw HttpNotFoundException();
954 else if (!isValidMetadataKind(kind, true))
955 throw ApiException("Unsupported metadata kind '" + kind + "'");
956
957 document["type"] = "Metadata";
958 document["kind"] = kind;
959
960 for (const string& i : metadata)
961 entries.push_back(i);
962
963 document["metadata"] = entries;
964 resp->setBody(document);
965 } else if (req->method == "PUT") {
966 auto document = req->json();
967
968 if (!isValidMetadataKind(kind, false))
969 throw ApiException("Unsupported metadata kind '" + kind + "'");
970
971 vector<string> vecMetadata;
972 auto& metadata = document["metadata"];
973 if (!metadata.is_array())
974 throw ApiException("metadata is not specified or not an array");
975
976 for (const auto& i : metadata.array_items()) {
977 if (!i.is_string())
978 throw ApiException("metadata must be strings");
979 vecMetadata.push_back(i.string_value());
980 }
981
982 if (!B.setDomainMetadata(zonename, kind, vecMetadata))
983 throw ApiException("Could not update metadata entries for domain '" + zonename.toString() + "'");
984
985 Json::object key {
986 { "type", "Metadata" },
987 { "kind", kind },
988 { "metadata", metadata }
989 };
990
991 resp->setBody(key);
992 } else if (req->method == "DELETE") {
993 if (!isValidMetadataKind(kind, false))
994 throw ApiException("Unsupported metadata kind '" + kind + "'");
995
996 vector<string> md; // an empty vector will do it
997 if (!B.setDomainMetadata(zonename, kind, md))
998 throw ApiException("Could not delete metadata for domain '" + zonename.toString() + "' (" + kind + ")");
999 } else
1000 throw HttpMethodNotAllowedException();
1001 }
1002
1003 // Throws 404 if the key with inquireKeyId does not exist
1004 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename, int inquireKeyId, DNSSECKeeper *dk) {
1005 DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);
1006 bool found = false;
1007 for(const auto& value : keyset) {
1008 if (value.second.id == (unsigned) inquireKeyId) {
1009 found = true;
1010 break;
1011 }
1012 }
1013 if (!found) {
1014 throw HttpNotFoundException();
1015 }
1016 }
1017
1018 static void apiZoneCryptokeysGET(DNSName zonename, int inquireKeyId, HttpResponse *resp, DNSSECKeeper *dk) {
1019 DNSSECKeeper::keyset_t keyset=dk->getKeys(zonename, false);
1020
1021 bool inquireSingleKey = inquireKeyId >= 0;
1022
1023 Json::array doc;
1024 for(const auto& value : keyset) {
1025 if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
1026 continue;
1027 }
1028
1029 string keyType;
1030 switch (value.second.keyType) {
1031 case DNSSECKeeper::KSK: keyType="ksk"; break;
1032 case DNSSECKeeper::ZSK: keyType="zsk"; break;
1033 case DNSSECKeeper::CSK: keyType="csk"; break;
1034 }
1035
1036 Json::object key {
1037 { "type", "Cryptokey" },
1038 { "id", (int)value.second.id },
1039 { "active", value.second.active },
1040 { "keytype", keyType },
1041 { "flags", (uint16_t)value.first.d_flags },
1042 { "dnskey", value.first.getDNSKEY().getZoneRepresentation() },
1043 { "algorithm", DNSSECKeeper::algorithm2name(value.first.d_algorithm) },
1044 { "bits", value.first.getKey()->getBits() }
1045 };
1046
1047 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
1048 Json::array dses;
1049 for(const uint8_t keyid : { DNSSECKeeper::SHA1, DNSSECKeeper::SHA256, DNSSECKeeper::GOST, DNSSECKeeper::SHA384 })
1050 try {
1051 dses.push_back(makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation());
1052 } catch (...) {}
1053 key["ds"] = dses;
1054 }
1055
1056 if (inquireSingleKey) {
1057 key["privatekey"] = value.first.getKey()->convertToISC();
1058 resp->setBody(key);
1059 return;
1060 }
1061 doc.push_back(key);
1062 }
1063
1064 if (inquireSingleKey) {
1065 // we came here because we couldn't find the requested key.
1066 throw HttpNotFoundException();
1067 }
1068 resp->setBody(doc);
1069
1070 }
1071
1072 /*
1073 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1074 * It deletes a key from :zone_name specified by :cryptokey_id.
1075 * Server Answers:
1076 * Case 1: the backend returns true on removal. This means the key is gone.
1077 * The server returns 204 No Content, no body.
1078 * Case 2: the backend returns false on removal. An error occurred.
1079 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1080 * Case 3: the key or zone does not exist.
1081 * The server returns 404 Not Found
1082 * */
1083 static void apiZoneCryptokeysDELETE(DNSName zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
1084 if (dk->removeKey(zonename, inquireKeyId)) {
1085 resp->body = "";
1086 resp->status = 204;
1087 } else {
1088 resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422);
1089 }
1090 }
1091
1092 /*
1093 * This method adds a key to a zone by generate it or content parameter.
1094 * Parameter:
1095 * {
1096 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1097 * "keytype" : "ksk|zsk" <string>
1098 * "active" : "true|false" <value>
1099 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1100 * "bits" : number of bits <int>
1101 * }
1102 *
1103 * Response:
1104 * Case 1: keytype isn't ksk|zsk
1105 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1106 * Case 2: 'bits' must be a positive integer value.
1107 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1108 * Case 3: The "algorithm" isn't supported
1109 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1110 * Case 4: Algorithm <= 10 and no bits were passed
1111 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1112 * Case 5: The wrong keysize was passed
1113 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1114 * Case 6: If the server cant guess the keysize
1115 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1116 * Case 7: The key-creation failed
1117 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1118 * Case 8: The key in content has the wrong format
1119 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1120 * Case 9: The wrong combination of fields is submitted
1121 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1122 * Case 10: No content and everything was fine
1123 * The server returns 201 Created and all public data about the new cryptokey
1124 * Case 11: With specified content
1125 * The server returns 201 Created and all public data about the added cryptokey
1126 */
1127
1128 static void apiZoneCryptokeysPOST(DNSName zonename, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
1129 auto document = req->json();
1130 string privatekey_fieldname = "privatekey";
1131 auto privatekey = document["privatekey"];
1132 if (privatekey.is_null()) {
1133 // Fallback to the old "content" behaviour
1134 privatekey = document["content"];
1135 privatekey_fieldname = "content";
1136 }
1137 bool active = boolFromJson(document, "active", false);
1138 bool keyOrZone;
1139
1140 if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") {
1141 keyOrZone = true;
1142 } else if (stringFromJson(document, "keytype") == "zsk") {
1143 keyOrZone = false;
1144 } else {
1145 throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
1146 }
1147
1148 int64_t insertedId = -1;
1149
1150 if (privatekey.is_null()) {
1151 int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1152 auto docbits = document["bits"];
1153 if (!docbits.is_null()) {
1154 if (!docbits.is_number() || (fmod(docbits.number_value(), 1.0) != 0) || docbits.int_value() < 0) {
1155 throw ApiException("'bits' must be a positive integer value");
1156 } else {
1157 bits = docbits.int_value();
1158 }
1159 }
1160 int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1161 auto providedAlgo = document["algorithm"];
1162 if (providedAlgo.is_string()) {
1163 algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
1164 if (algorithm == -1)
1165 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
1166 } else if (providedAlgo.is_number()) {
1167 algorithm = providedAlgo.int_value();
1168 } else if (!providedAlgo.is_null()) {
1169 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
1170 }
1171
1172 try {
1173 if (!dk->addKey(zonename, keyOrZone, algorithm, insertedId, bits, active)) {
1174 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1175 }
1176 } catch (std::runtime_error& error) {
1177 throw ApiException(error.what());
1178 }
1179 if (insertedId < 0)
1180 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1181 } else if (document["bits"].is_null() && document["algorithm"].is_null()) {
1182 auto keyData = stringFromJson(document, privatekey_fieldname);
1183 DNSKEYRecordContent dkrc;
1184 DNSSECPrivateKey dpk;
1185 try {
1186 shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
1187 dpk.d_algorithm = dkrc.d_algorithm;
1188 // TODO remove in 4.2.0
1189 if(dpk.d_algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1)
1190 dpk.d_algorithm = DNSSECKeeper::RSASHA1;
1191
1192 if (keyOrZone)
1193 dpk.d_flags = 257;
1194 else
1195 dpk.d_flags = 256;
1196
1197 dpk.setKey(dke);
1198 }
1199 catch (std::runtime_error& error) {
1200 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1201 } try {
1202 if (!dk->addKey(zonename, dpk,insertedId, active)) {
1203 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1204 }
1205 } catch (std::runtime_error& error) {
1206 throw ApiException(error.what());
1207 }
1208 if (insertedId < 0)
1209 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1210 } else {
1211 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1212 }
1213 apiZoneCryptokeysGET(zonename, insertedId, resp, dk);
1214 resp->status = 201;
1215 }
1216
1217 /*
1218 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1219 * It de/activates a key from :zone_name specified by :cryptokey_id.
1220 * Server Answers:
1221 * Case 1: invalid JSON data
1222 * The server returns 400 Bad Request
1223 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1224 * The server returns 204 No Content
1225 * Case 3: the backend returns false on de/activation. An error occurred.
1226 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1227 * */
1228 static void apiZoneCryptokeysPUT(DNSName zonename, int inquireKeyId, HttpRequest *req, HttpResponse *resp, DNSSECKeeper *dk) {
1229 //throws an exception if the Body is empty
1230 auto document = req->json();
1231 //throws an exception if the key does not exist or is not a bool
1232 bool active = boolFromJson(document, "active");
1233 if (active) {
1234 if (!dk->activateKey(zonename, inquireKeyId)) {
1235 resp->setErrorResult("Could not activate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
1236 return;
1237 }
1238 } else {
1239 if (!dk->deactivateKey(zonename, inquireKeyId)) {
1240 resp->setErrorResult("Could not deactivate Key: " + req->parameters["key_id"] + " in Zone: " + zonename.toString(), 422);
1241 return;
1242 }
1243 }
1244 resp->body = "";
1245 resp->status = 204;
1246 return;
1247 }
1248
1249 /*
1250 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1251 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1252 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1253 * */
1254 static void apiZoneCryptokeys(HttpRequest *req, HttpResponse *resp) {
1255 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1256
1257 UeberBackend B;
1258 DNSSECKeeper dk(&B);
1259 DomainInfo di;
1260 if (!B.getDomainInfo(zonename, di)) {
1261 throw HttpNotFoundException();
1262 }
1263
1264 int inquireKeyId = -1;
1265 if (req->parameters.count("key_id")) {
1266 inquireKeyId = std::stoi(req->parameters["key_id"]);
1267 apiZoneCryptoKeysCheckKeyExists(zonename, inquireKeyId, &dk);
1268 }
1269
1270 if (req->method == "GET") {
1271 apiZoneCryptokeysGET(zonename, inquireKeyId, resp, &dk);
1272 } else if (req->method == "DELETE") {
1273 if (inquireKeyId == -1)
1274 throw HttpBadRequestException();
1275 apiZoneCryptokeysDELETE(zonename, inquireKeyId, req, resp, &dk);
1276 } else if (req->method == "POST") {
1277 apiZoneCryptokeysPOST(zonename, req, resp, &dk);
1278 } else if (req->method == "PUT") {
1279 if (inquireKeyId == -1)
1280 throw HttpBadRequestException();
1281 apiZoneCryptokeysPUT(zonename, inquireKeyId, req, resp, &dk);
1282 } else {
1283 throw HttpMethodNotAllowedException(); //Returns method not allowed
1284 }
1285 }
1286
1287 static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, DNSName zonename) {
1288 DNSResourceRecord rr;
1289 vector<string> zonedata;
1290 stringtok(zonedata, zonestring, "\r\n");
1291
1292 ZoneParserTNG zpt(zonedata, zonename);
1293
1294 bool seenSOA=false;
1295
1296 string comment = "Imported via the API";
1297
1298 try {
1299 while(zpt.get(rr, &comment)) {
1300 if(seenSOA && rr.qtype.getCode() == QType::SOA)
1301 continue;
1302 if(rr.qtype.getCode() == QType::SOA)
1303 seenSOA=true;
1304 validateGatheredRRType(rr);
1305
1306 new_records.push_back(rr);
1307 }
1308 }
1309 catch(std::exception& ae) {
1310 throw ApiException("An error occurred while parsing the zonedata: "+string(ae.what()));
1311 }
1312 }
1313
1314 /** Throws ApiException if records which violate RRset contraints are present.
1315 * NOTE: sorts records in-place.
1316 *
1317 * Constraints being checked:
1318 * *) no exact duplicates
1319 * *) no duplicates for QTypes that can only be present once per RRset
1320 * *) hostnames are hostnames
1321 */
1322 static void checkNewRecords(vector<DNSResourceRecord>& records) {
1323 sort(records.begin(), records.end(),
1324 [](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool {
1325 /* we need _strict_ weak ordering */
1326 return std::tie(rec_a.qname, rec_a.qtype, rec_a.content) < std::tie(rec_b.qname, rec_b.qtype, rec_b.content);
1327 }
1328 );
1329
1330 DNSResourceRecord previous;
1331 for(const auto& rec : records) {
1332 if (previous.qname == rec.qname) {
1333 if (previous.qtype == rec.qtype) {
1334 if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
1335 throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName()+" has more than one record");
1336 }
1337 if (previous.content == rec.content) {
1338 throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.getName() + " with content \"" + rec.content + "\"");
1339 }
1340 } else if (exclusiveEntryTypes.count(rec.qtype.getCode()) != 0 || exclusiveEntryTypes.count(previous.qtype.getCode()) != 0) {
1341 throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName()+": Conflicts with another RRset");
1342 }
1343 }
1344
1345 // Check if the DNSNames that should be hostnames, are hostnames
1346 try {
1347 checkHostnameCorrectness(rec);
1348 } catch (const std::exception& e) {
1349 throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName() + " " + e.what());
1350 }
1351
1352 previous = rec;
1353 }
1354 }
1355
1356 static void checkTSIGKey(UeberBackend& B, const DNSName& keyname, const DNSName& algo, const string& content) {
1357 DNSName algoFromDB;
1358 string contentFromDB;
1359 B.getTSIGKey(keyname, &algoFromDB, &contentFromDB);
1360 if (!contentFromDB.empty() || !algoFromDB.empty()) {
1361 throw HttpConflictException("A TSIG key with the name '"+keyname.toLogString()+"' already exists");
1362 }
1363
1364 TSIGHashEnum the;
1365 if (!getTSIGHashEnum(algo, the)) {
1366 throw ApiException("Unknown TSIG algorithm: " + algo.toLogString());
1367 }
1368
1369 string b64out;
1370 if (B64Decode(content, b64out) == -1) {
1371 throw ApiException("TSIG content '" + content + "' cannot be base64-decoded");
1372 }
1373 }
1374
1375 static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content) {
1376 Json::object tsigkey = {
1377 { "name", keyname.toStringNoDot() },
1378 { "id", apiZoneNameToId(keyname) },
1379 { "algorithm", algo.toStringNoDot() },
1380 { "key", content },
1381 { "type", "TSIGKey" }
1382 };
1383 return tsigkey;
1384 }
1385
1386 static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent=true) {
1387 return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : "");
1388 }
1389
1390 static void apiServerTSIGKeys(HttpRequest* req, HttpResponse* resp) {
1391 UeberBackend B;
1392 if (req->method == "GET") {
1393 vector<struct TSIGKey> keys;
1394
1395 if (!B.getTSIGKeys(keys)) {
1396 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1397 }
1398
1399 Json::array doc;
1400
1401 for(const auto &key : keys) {
1402 doc.push_back(makeJSONTSIGKey(key, false));
1403 }
1404 resp->setBody(doc);
1405 } else if (req->method == "POST") {
1406 auto document = req->json();
1407 DNSName keyname(stringFromJson(document, "name"));
1408 DNSName algo(stringFromJson(document, "algorithm"));
1409 string content = document["key"].string_value();
1410
1411 if (content.empty()) {
1412 try {
1413 content = makeTSIGKey(algo);
1414 } catch (const PDNSException& e) {
1415 throw HttpBadRequestException(e.reason);
1416 }
1417 }
1418
1419 // Will throw an ApiException or HttpConflictException on error
1420 checkTSIGKey(B, keyname, algo, content);
1421
1422 if(!B.setTSIGKey(keyname, algo, content)) {
1423 throw HttpInternalServerErrorException("Unable to add TSIG key");
1424 }
1425
1426 resp->status = 201;
1427 resp->setBody(makeJSONTSIGKey(keyname, algo, content));
1428 } else {
1429 throw HttpMethodNotAllowedException();
1430 }
1431 }
1432
1433 static void apiServerTSIGKeyDetail(HttpRequest* req, HttpResponse* resp) {
1434 UeberBackend B;
1435 DNSName keyname = apiZoneIdToName(req->parameters["id"]);
1436 DNSName algo;
1437 string content;
1438
1439 if (!B.getTSIGKey(keyname, &algo, &content)) {
1440 throw HttpNotFoundException("TSIG key with name '"+keyname.toLogString()+"' not found");
1441 }
1442
1443 struct TSIGKey tsk;
1444 tsk.name = keyname;
1445 tsk.algorithm = algo;
1446 tsk.key = content;
1447
1448 if (req->method == "GET") {
1449 resp->setBody(makeJSONTSIGKey(tsk));
1450 } else if (req->method == "PUT") {
1451 json11::Json document;
1452 if (!req->body.empty()) {
1453 document = req->json();
1454 }
1455 if (document["name"].is_string()) {
1456 tsk.name = DNSName(document["name"].string_value());
1457 }
1458 if (document["algorithm"].is_string()) {
1459 tsk.algorithm = DNSName(document["algorithm"].string_value());
1460
1461 TSIGHashEnum the;
1462 if (!getTSIGHashEnum(tsk.algorithm, the)) {
1463 throw ApiException("Unknown TSIG algorithm: " + tsk.algorithm.toLogString());
1464 }
1465 }
1466 if (document["key"].is_string()) {
1467 string new_content = document["key"].string_value();
1468 string decoded;
1469 if (B64Decode(new_content, decoded) == -1) {
1470 throw ApiException("Can not base64 decode key content '" + new_content + "'");
1471 }
1472 tsk.key = new_content;
1473 }
1474 if (!B.setTSIGKey(tsk.name, tsk.algorithm, tsk.key)) {
1475 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1476 }
1477 if (tsk.name != keyname) {
1478 // Remove the old key
1479 if (!B.deleteTSIGKey(keyname)) {
1480 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
1481 }
1482 }
1483 resp->setBody(makeJSONTSIGKey(tsk));
1484 } else if (req->method == "DELETE") {
1485 if (!B.deleteTSIGKey(keyname)) {
1486 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname.toStringNoDot() + "'");
1487 } else {
1488 resp->body = "";
1489 resp->status = 204;
1490 }
1491 } else {
1492 throw HttpMethodNotAllowedException();
1493 }
1494 }
1495
1496 static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
1497 UeberBackend B;
1498 DNSSECKeeper dk(&B);
1499 if (req->method == "POST") {
1500 DomainInfo di;
1501 auto document = req->json();
1502 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1503 apiCheckNameAllowedCharacters(zonename.toString());
1504 zonename.makeUsLowerCase();
1505
1506 bool exists = B.getDomainInfo(zonename, di);
1507 if(exists)
1508 throw HttpConflictException();
1509
1510 // validate 'kind' is set
1511 DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
1512
1513 string zonestring = document["zone"].string_value();
1514 auto rrsets = document["rrsets"];
1515 if (rrsets.is_array() && zonestring != "")
1516 throw ApiException("You cannot give rrsets AND zone data as text");
1517
1518 auto nameservers = document["nameservers"];
1519 if (!nameservers.is_array() && zonekind != DomainInfo::Slave)
1520 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
1521
1522 string soa_edit_api_kind;
1523 if (document["soa_edit_api"].is_string()) {
1524 soa_edit_api_kind = document["soa_edit_api"].string_value();
1525 }
1526 else {
1527 soa_edit_api_kind = "DEFAULT";
1528 }
1529 string soa_edit_kind = document["soa_edit"].string_value();
1530
1531 // if records/comments are given, load and check them
1532 bool have_soa = false;
1533 bool have_zone_ns = false;
1534 vector<DNSResourceRecord> new_records;
1535 vector<Comment> new_comments;
1536 vector<DNSResourceRecord> new_ptrs;
1537
1538 if (rrsets.is_array()) {
1539 for (const auto& rrset : rrsets.array_items()) {
1540 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1541 apiCheckQNameAllowedCharacters(qname.toString());
1542 QType qtype;
1543 qtype = stringFromJson(rrset, "type");
1544 if (qtype.getCode() == 0) {
1545 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1546 }
1547 if (rrset["records"].is_array()) {
1548 int ttl = intFromJson(rrset, "ttl");
1549 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
1550 }
1551 if (rrset["comments"].is_array()) {
1552 gatherComments(rrset, qname, qtype, new_comments);
1553 }
1554 }
1555 } else if (zonestring != "") {
1556 gatherRecordsFromZone(zonestring, new_records, zonename);
1557 }
1558
1559 for(auto& rr : new_records) {
1560 rr.qname.makeUsLowerCase();
1561 if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
1562 throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
1563 apiCheckQNameAllowedCharacters(rr.qname.toString());
1564
1565 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
1566 have_soa = true;
1567 increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1568 }
1569 if (rr.qtype.getCode() == QType::NS && rr.qname==zonename) {
1570 have_zone_ns = true;
1571 }
1572 }
1573
1574 // synthesize RRs as needed
1575 DNSResourceRecord autorr;
1576 autorr.qname = zonename;
1577 autorr.auth = 1;
1578 autorr.ttl = ::arg().asNum("default-ttl");
1579
1580 if (!have_soa && zonekind != DomainInfo::Slave) {
1581 // synthesize a SOA record so the zone "really" exists
1582 string soa = (boost::format("%s %s %ul")
1583 % ::arg()["default-soa-name"]
1584 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
1585 % document["serial"].int_value()
1586 ).str();
1587 SOAData sd;
1588 fillSOAData(soa, sd); // fills out default values for us
1589 autorr.qtype = QType::SOA;
1590 autorr.content = makeSOAContent(sd)->getZoneRepresentation(true);
1591 increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
1592 new_records.push_back(autorr);
1593 }
1594
1595 // create NS records if nameservers are given
1596 for (auto value : nameservers.array_items()) {
1597 string nameserver = value.string_value();
1598 if (nameserver.empty())
1599 throw ApiException("Nameservers must be non-empty strings");
1600 if (!isCanonical(nameserver))
1601 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
1602 try {
1603 // ensure the name parses
1604 autorr.content = DNSName(nameserver).toStringRootDot();
1605 } catch (...) {
1606 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
1607 }
1608 autorr.qtype = QType::NS;
1609 new_records.push_back(autorr);
1610 if (have_zone_ns) {
1611 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1612 }
1613 }
1614
1615 checkNewRecords(new_records);
1616
1617 if (boolFromJson(document, "dnssec", false)) {
1618 checkDefaultDNSSECAlgos();
1619
1620 if(document["nsec3param"].string_value().length() > 0) {
1621 NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
1622 string error_msg = "";
1623 if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
1624 throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
1625 }
1626 }
1627 }
1628
1629 // no going back after this
1630 if(!B.createDomain(zonename))
1631 throw ApiException("Creating domain '"+zonename.toString()+"' failed");
1632
1633 if(!B.getDomainInfo(zonename, di))
1634 throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
1635
1636 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1637 if (!soa_edit_api_kind.empty()) {
1638 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1639 }
1640
1641 di.backend->startTransaction(zonename, di.id);
1642
1643 for(auto rr : new_records) {
1644 rr.domain_id = di.id;
1645 di.backend->feedRecord(rr, DNSName());
1646 }
1647 for(Comment& c : new_comments) {
1648 c.domain_id = di.id;
1649 di.backend->feedComment(c);
1650 }
1651
1652 updateDomainSettingsFromDocument(B, di, zonename, document);
1653
1654 di.backend->commitTransaction();
1655
1656 storeChangedPTRs(B, new_ptrs);
1657
1658 fillZone(zonename, resp, shouldDoRRSets(req));
1659 resp->status = 201;
1660 return;
1661 }
1662
1663 if(req->method != "GET")
1664 throw HttpMethodNotAllowedException();
1665
1666 vector<DomainInfo> domains;
1667
1668 if (req->getvars.count("zone")) {
1669 string zone = req->getvars["zone"];
1670 apiCheckNameAllowedCharacters(zone);
1671 DNSName zonename = apiNameToDNSName(zone);
1672 zonename.makeUsLowerCase();
1673 DomainInfo di;
1674 if (B.getDomainInfo(zonename, di)) {
1675 domains.push_back(di);
1676 }
1677 } else {
1678 B.getAllDomains(&domains, true); // incl. disabled
1679 }
1680
1681 Json::array doc;
1682 for(const DomainInfo& di : domains) {
1683 doc.push_back(getZoneInfo(di, &dk));
1684 }
1685 resp->setBody(doc);
1686 }
1687
1688 static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) {
1689 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1690
1691 UeberBackend B;
1692 DomainInfo di;
1693 if (!B.getDomainInfo(zonename, di)) {
1694 throw HttpNotFoundException();
1695 }
1696
1697 if(req->method == "PUT") {
1698 // update domain settings
1699
1700 updateDomainSettingsFromDocument(B, di, zonename, req->json());
1701
1702 resp->body = "";
1703 resp->status = 204; // No Content, but indicate success
1704 return;
1705 }
1706 else if(req->method == "DELETE") {
1707 // delete domain
1708 if(!di.backend->deleteDomain(zonename))
1709 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
1710
1711 // clear caches
1712 DNSSECKeeper dk(&B);
1713 dk.clearCaches(zonename);
1714 purgeAuthCaches(zonename.toString() + "$");
1715
1716 // empty body on success
1717 resp->body = "";
1718 resp->status = 204; // No Content: declare that the zone is gone now
1719 return;
1720 } else if (req->method == "PATCH") {
1721 patchZone(req, resp);
1722 return;
1723 } else if (req->method == "GET") {
1724 fillZone(zonename, resp, shouldDoRRSets(req));
1725 return;
1726 }
1727 throw HttpMethodNotAllowedException();
1728 }
1729
1730 static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
1731 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1732
1733 if(req->method != "GET")
1734 throw HttpMethodNotAllowedException();
1735
1736 ostringstream ss;
1737
1738 UeberBackend B;
1739 DomainInfo di;
1740 if (!B.getDomainInfo(zonename, di)) {
1741 throw HttpNotFoundException();
1742 }
1743
1744 DNSResourceRecord rr;
1745 SOAData sd;
1746 di.backend->list(zonename, di.id);
1747 while(di.backend->get(rr)) {
1748 if (!rr.qtype.getCode())
1749 continue; // skip empty non-terminals
1750
1751 ss <<
1752 rr.qname.toString() << "\t" <<
1753 rr.ttl << "\t" <<
1754 "IN" << "\t" <<
1755 rr.qtype.getName() << "\t" <<
1756 makeApiRecordContent(rr.qtype, rr.content) <<
1757 endl;
1758 }
1759
1760 if (req->accept_json) {
1761 resp->setBody(Json::object { { "zone", ss.str() } });
1762 } else {
1763 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
1764 resp->body = ss.str();
1765 }
1766 }
1767
1768 static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
1769 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1770
1771 if(req->method != "PUT")
1772 throw HttpMethodNotAllowedException();
1773
1774 UeberBackend B;
1775 DomainInfo di;
1776 if (!B.getDomainInfo(zonename, di)) {
1777 throw HttpNotFoundException();
1778 }
1779
1780 if(di.masters.empty())
1781 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
1782
1783 random_shuffle(di.masters.begin(), di.masters.end());
1784 Communicator.addSuckRequest(zonename, di.masters.front());
1785 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front().toLogString());
1786 }
1787
1788 static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
1789 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1790
1791 if(req->method != "PUT")
1792 throw HttpMethodNotAllowedException();
1793
1794 UeberBackend B;
1795 DomainInfo di;
1796 if (!B.getDomainInfo(zonename, di)) {
1797 throw HttpNotFoundException();
1798 }
1799
1800 if(!Communicator.notifyDomain(zonename))
1801 throw ApiException("Failed to add to the queue - see server log");
1802
1803 resp->setSuccessResult("Notification queued");
1804 }
1805
1806 static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp) {
1807 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1808
1809 if(req->method != "PUT")
1810 throw HttpMethodNotAllowedException();
1811
1812 UeberBackend B;
1813 DomainInfo di;
1814 if (!B.getDomainInfo(zonename, di)) {
1815 throw HttpNotFoundException();
1816 }
1817
1818 DNSSECKeeper dk(&B);
1819
1820 if (!dk.isSecuredZone(zonename))
1821 throw ApiException("Zone '" + zonename.toString() + "' is not DNSSEC signed, not rectifying.");
1822
1823 if (di.kind == DomainInfo::Slave)
1824 throw ApiException("Zone '" + zonename.toString() + "' is a slave zone, not rectifying.");
1825
1826 string error_msg = "";
1827 string info;
1828 if (!dk.rectifyZone(zonename, error_msg, info, true))
1829 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
1830
1831 resp->setSuccessResult("Rectified");
1832 }
1833
1834 static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
1835 if (rr.qtype.getCode() == QType::A) {
1836 uint32_t ip;
1837 if (!IpToU32(rr.content, &ip)) {
1838 throw ApiException("PTR: Invalid IP address given");
1839 }
1840 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1841 % ((ip >> 24) & 0xff)
1842 % ((ip >> 16) & 0xff)
1843 % ((ip >> 8) & 0xff)
1844 % ((ip ) & 0xff)
1845 ).str());
1846 } else if (rr.qtype.getCode() == QType::AAAA) {
1847 ComboAddress ca(rr.content);
1848 char buf[3];
1849 ostringstream ss;
1850 for (int octet = 0; octet < 16; ++octet) {
1851 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
1852 // this should be impossible: no byte should give more than two digits in hex format
1853 throw PDNSException("Formatting IPv6 address failed");
1854 }
1855 ss << buf[0] << '.' << buf[1] << '.';
1856 }
1857 string tmp = ss.str();
1858 tmp.resize(tmp.size()-1); // remove last dot
1859 // reverse and append arpa domain
1860 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
1861 } else {
1862 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
1863 }
1864
1865 ptr->qtype = "PTR";
1866 ptr->ttl = rr.ttl;
1867 ptr->disabled = rr.disabled;
1868 ptr->content = rr.qname.toStringRootDot();
1869 }
1870
1871 static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs) {
1872 for(const DNSResourceRecord& rr : new_ptrs) {
1873 SOAData sd;
1874 if (!B.getAuth(rr.qname, QType(QType::PTR), &sd, false))
1875 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
1876
1877 string soa_edit_api_kind;
1878 string soa_edit_kind;
1879 bool soa_changed = false;
1880 DNSResourceRecord soarr;
1881 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT-API", soa_edit_api_kind);
1882 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT", soa_edit_kind);
1883 if (!soa_edit_api_kind.empty()) {
1884 soa_changed = makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, soarr);
1885 }
1886
1887 sd.db->startTransaction(sd.qname);
1888 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1889 sd.db->abortTransaction();
1890 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
1891 }
1892
1893 if (soa_changed) {
1894 sd.db->replaceRRSet(sd.domain_id, soarr.qname, soarr.qtype, vector<DNSResourceRecord>(1, soarr));
1895 }
1896
1897 sd.db->commitTransaction();
1898 purgeAuthCachesExact(rr.qname);
1899 }
1900 }
1901
1902 static void patchZone(HttpRequest* req, HttpResponse* resp) {
1903 UeberBackend B;
1904 DomainInfo di;
1905 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1906 if (!B.getDomainInfo(zonename, di)) {
1907 throw HttpNotFoundException();
1908 }
1909
1910 vector<DNSResourceRecord> new_records;
1911 vector<Comment> new_comments;
1912 vector<DNSResourceRecord> new_ptrs;
1913
1914 Json document = req->json();
1915
1916 auto rrsets = document["rrsets"];
1917 if (!rrsets.is_array())
1918 throw ApiException("No rrsets given in update request");
1919
1920 di.backend->startTransaction(zonename);
1921
1922 try {
1923 string soa_edit_api_kind;
1924 string soa_edit_kind;
1925 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1926 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
1927 bool soa_edit_done = false;
1928
1929 set<pair<DNSName, QType>> seen;
1930
1931 for (const auto& rrset : rrsets.array_items()) {
1932 string changetype = toUpper(stringFromJson(rrset, "changetype"));
1933 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1934 apiCheckQNameAllowedCharacters(qname.toString());
1935 QType qtype;
1936 qtype = stringFromJson(rrset, "type");
1937 if (qtype.getCode() == 0) {
1938 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1939 }
1940
1941 if(seen.count({qname, qtype}))
1942 {
1943 throw ApiException("Duplicate RRset "+qname.toString()+" IN "+qtype.getName());
1944 }
1945 seen.insert({qname, qtype});
1946
1947 if (changetype == "DELETE") {
1948 // delete all matching qname/qtype RRs (and, implicitly comments).
1949 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
1950 throw ApiException("Hosting backend does not support editing records.");
1951 }
1952 }
1953 else if (changetype == "REPLACE") {
1954 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1955 if (!qname.isPartOf(zonename) && qname != zonename)
1956 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
1957
1958 bool replace_records = rrset["records"].is_array();
1959 bool replace_comments = rrset["comments"].is_array();
1960
1961 if (!replace_records && !replace_comments) {
1962 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.getName());
1963 }
1964
1965 new_records.clear();
1966 new_comments.clear();
1967
1968 if (replace_records) {
1969 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1970 int ttl = intFromJson(rrset, "ttl");
1971 // new_ptrs is merged.
1972 gatherRecords(rrset, qname, qtype, ttl, new_records, new_ptrs);
1973
1974 for(DNSResourceRecord& rr : new_records) {
1975 rr.domain_id = di.id;
1976 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
1977 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1978 }
1979 }
1980 checkNewRecords(new_records);
1981 }
1982
1983 if (replace_comments) {
1984 gatherComments(rrset, qname, qtype, new_comments);
1985
1986 for(Comment& c : new_comments) {
1987 c.domain_id = di.id;
1988 }
1989 }
1990
1991 if (replace_records) {
1992 bool ent_present = false;
1993 di.backend->lookup(QType(QType::ANY), qname);
1994 DNSResourceRecord rr;
1995 while (di.backend->get(rr)) {
1996 if (qtype.getCode() == 0) {
1997 ent_present = true;
1998 }
1999 if (qtype.getCode() != rr.qtype.getCode()
2000 && (exclusiveEntryTypes.count(qtype.getCode()) != 0
2001 || exclusiveEntryTypes.count(rr.qtype.getCode()) != 0)) {
2002 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Conflicts with pre-existing RRset");
2003 }
2004 }
2005
2006 if (!new_records.empty() && ent_present) {
2007 QType qt_ent{0};
2008 if (!di.backend->replaceRRSet(di.id, qname, qt_ent, new_records)) {
2009 throw ApiException("Hosting backend does not support editing records.");
2010 }
2011 }
2012 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
2013 throw ApiException("Hosting backend does not support editing records.");
2014 }
2015 }
2016 if (replace_comments) {
2017 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
2018 throw ApiException("Hosting backend does not support editing comments.");
2019 }
2020 }
2021 }
2022 else
2023 throw ApiException("Changetype not understood");
2024 }
2025
2026 // edit SOA (if needed)
2027 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
2028 SOAData sd;
2029 if (!B.getSOAUncached(zonename, sd))
2030 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
2031
2032 DNSResourceRecord rr;
2033 if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
2034 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
2035 throw ApiException("Hosting backend does not support editing records.");
2036 }
2037 }
2038
2039 // return old and new serials in headers
2040 resp->headers["X-PDNS-Old-Serial"] = std::to_string(sd.serial);
2041 fillSOAData(rr.content, sd);
2042 resp->headers["X-PDNS-New-Serial"] = std::to_string(sd.serial);
2043 }
2044
2045 } catch(...) {
2046 di.backend->abortTransaction();
2047 throw;
2048 }
2049
2050 DNSSECKeeper dk(&B);
2051 string api_rectify;
2052 di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
2053 if (dk.isSecuredZone(zonename) && !dk.isPresigned(zonename) && api_rectify == "1") {
2054 string error_msg = "";
2055 string info;
2056 if (!dk.rectifyZone(zonename, error_msg, info, false))
2057 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
2058 }
2059
2060 di.backend->commitTransaction();
2061
2062 purgeAuthCachesExact(zonename);
2063
2064 // now the PTRs
2065 storeChangedPTRs(B, new_ptrs);
2066
2067 resp->body = "";
2068 resp->status = 204; // No Content, but indicate success
2069 return;
2070 }
2071
2072 static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
2073 if(req->method != "GET")
2074 throw HttpMethodNotAllowedException();
2075
2076 string q = req->getvars["q"];
2077 string sMax = req->getvars["max"];
2078 string sObjectType = req->getvars["object_type"];
2079
2080 int maxEnts = 100;
2081 int ents = 0;
2082
2083 // the following types of data can be searched for using the api
2084 enum class ObjectType
2085 {
2086 ALL,
2087 ZONE,
2088 RECORD,
2089 COMMENT
2090 } objectType;
2091
2092 if (q.empty())
2093 throw ApiException("Query q can't be blank");
2094 if (!sMax.empty())
2095 maxEnts = std::stoi(sMax);
2096 if (maxEnts < 1)
2097 throw ApiException("Maximum entries must be larger than 0");
2098
2099 if (sObjectType.empty())
2100 objectType = ObjectType::ALL;
2101 else if (sObjectType == "all")
2102 objectType = ObjectType::ALL;
2103 else if (sObjectType == "zone")
2104 objectType = ObjectType::ZONE;
2105 else if (sObjectType == "record")
2106 objectType = ObjectType::RECORD;
2107 else if (sObjectType == "comment")
2108 objectType = ObjectType::COMMENT;
2109 else
2110 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2111
2112 SimpleMatch sm(q,true);
2113 UeberBackend B;
2114 vector<DomainInfo> domains;
2115 vector<DNSResourceRecord> result_rr;
2116 vector<Comment> result_c;
2117 map<int,DomainInfo> zoneIdZone;
2118 map<int,DomainInfo>::iterator val;
2119 Json::array doc;
2120
2121 B.getAllDomains(&domains, true);
2122
2123 for(const DomainInfo di: domains)
2124 {
2125 if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && sm.match(di.zone)) {
2126 doc.push_back(Json::object {
2127 { "object_type", "zone" },
2128 { "zone_id", apiZoneNameToId(di.zone) },
2129 { "name", di.zone.toString() }
2130 });
2131 ents++;
2132 }
2133 zoneIdZone[di.id] = di; // populate cache
2134 }
2135
2136 if ((objectType == ObjectType::ALL || objectType == ObjectType::RECORD) && B.searchRecords(q, maxEnts, result_rr))
2137 {
2138 for(const DNSResourceRecord& rr: result_rr)
2139 {
2140 if (!rr.qtype.getCode())
2141 continue; // skip empty non-terminals
2142
2143 auto object = Json::object {
2144 { "object_type", "record" },
2145 { "name", rr.qname.toString() },
2146 { "type", rr.qtype.getName() },
2147 { "ttl", (double)rr.ttl },
2148 { "disabled", rr.disabled },
2149 { "content", makeApiRecordContent(rr.qtype, rr.content) }
2150 };
2151 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
2152 object["zone_id"] = apiZoneNameToId(val->second.zone);
2153 object["zone"] = val->second.zone.toString();
2154 }
2155 doc.push_back(object);
2156 }
2157 }
2158
2159 if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && B.searchComments(q, maxEnts, result_c))
2160 {
2161 for(const Comment &c: result_c)
2162 {
2163 auto object = Json::object {
2164 { "object_type", "comment" },
2165 { "name", c.qname.toString() },
2166 { "content", c.content }
2167 };
2168 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
2169 object["zone_id"] = apiZoneNameToId(val->second.zone);
2170 object["zone"] = val->second.zone.toString();
2171 }
2172 doc.push_back(object);
2173 }
2174 }
2175
2176 resp->setBody(doc);
2177 }
2178
2179 void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
2180 if(req->method != "PUT")
2181 throw HttpMethodNotAllowedException();
2182
2183 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
2184
2185 uint64_t count = purgeAuthCachesExact(canon);
2186 resp->setBody(Json::object {
2187 { "count", (int) count },
2188 { "result", "Flushed cache." }
2189 });
2190 }
2191
2192 void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
2193 {
2194 resp->headers["Cache-Control"] = "max-age=86400";
2195 resp->headers["Content-Type"] = "text/css";
2196
2197 ostringstream ret;
2198 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
2199 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
2200 ret<<"a { color: #0959c2; }"<<endl;
2201 ret<<"a:hover { color: #3B8EC8; }"<<endl;
2202 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
2203 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
2204 ret<<".row:after { clear: both; }"<<endl;
2205 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
2206 ret<<".all { width: 100%; }"<<endl;
2207 ret<<".headl { width: 60%; }"<<endl;
2208 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2209 ret<<"background-image: url(data:image/png;base64,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);";
2210 ret<<" width: 154px; height: 20px; }"<<endl;
2211 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
2212 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
2213 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
2214 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
2215 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
2216 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
2217 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
2218 ret<<"table.data tr:hover { background: white; }"<<endl;
2219 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
2220 ret<<".resetring {float: right; }"<<endl;
2221 ret<<".resetring i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA/klEQVQY01XPP04UUBgE8N/33vd2XZUWEuzYuMZEG4KFCQn2NhA4AIewAOMBPIG2xhNYeAcKGqkNCdmYlVBZGBIT4FHsbuE0U8xk/kAbqm9TOfI/nicfhmwgDNhvylUT58kxCp4l31L8SfH9IetJ2ev6PwyIwyZWsdb11/gbTK55Co+r8rmJaRPTFJcpZil+pTit7C5awMpA+Zpi1sRFE9MqflYOloYCjY2uP8EdYiGU4CVGUBubxKfOOLjrtOBmzvEilbVb/aQWvhRl0unBZVXe4XdnK+bprwqnhoyTsyZ+JG8Wk0apfExxlcp7PFruXH8gdxamWB4cyW2sIO4BG3czIp78jUIAAAAASUVORK5CYII=); width: 10px; height: 10px; margin-right: 2px; display: inline-block; background-repeat: no-repeat; }"<<endl;
2222 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
2223 ret<<".resizering {float: right;}"<<endl;
2224 resp->body = ret.str();
2225 resp->status = 200;
2226 }
2227
2228 void AuthWebServer::webThread()
2229 {
2230 try {
2231 setThreadName("pdns/webserver");
2232 if(::arg().mustDo("api")) {
2233 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
2234 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
2235 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
2236 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
2237 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail);
2238 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys);
2239 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
2240 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
2241 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
2242 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
2243 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind);
2244 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata);
2245 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
2246 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify);
2247 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
2248 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
2249 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
2250 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
2251 d_ws->registerApiHandler("/api", &apiDiscovery);
2252 }
2253 if (::arg().mustDo("webserver")) {
2254 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
2255 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
2256 }
2257 d_ws->go();
2258 }
2259 catch(...) {
2260 g_log<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
2261 _exit(1);
2262 }
2263 }