]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/ws-auth.cc
Merge pull request #13768 from rgacogne/ddist-maintenance-hook
[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 #include "dnsbackend.hh"
23 #include "webserver.hh"
24 #include <array>
25 #ifdef HAVE_CONFIG_H
26 #include "config.h"
27 #endif
28 #include "utility.hh"
29 #include "dynlistener.hh"
30 #include "ws-auth.hh"
31 #include "json.hh"
32 #include "logger.hh"
33 #include "statbag.hh"
34 #include "misc.hh"
35 #include "base64.hh"
36 #include "arguments.hh"
37 #include "dns.hh"
38 #include "comment.hh"
39 #include "ueberbackend.hh"
40 #include <boost/format.hpp>
41
42 #include "namespaces.hh"
43 #include "ws-api.hh"
44 #include "version.hh"
45 #include "dnsseckeeper.hh"
46 #include <iomanip>
47 #include "zoneparser-tng.hh"
48 #include "auth-main.hh"
49 #include "auth-caches.hh"
50 #include "auth-zonecache.hh"
51 #include "threadname.hh"
52 #include "tsigutils.hh"
53
54 using json11::Json;
55
56 Ewma::Ewma() { dt.set(); }
57
58 void Ewma::submit(int val)
59 {
60 int rate = val - d_last;
61 double difft = dt.udiff() / 1000000.0;
62 dt.set();
63
64 d_10 = ((600.0 - difft) * d_10 + (difft * rate)) / 600.0;
65 d_5 = ((300.0 - difft) * d_5 + (difft * rate)) / 300.0;
66 d_1 = ((60.0 - difft) * d_1 + (difft * rate)) / 60.0;
67 d_max = max(d_1, d_max);
68
69 d_last = val;
70 }
71
72 double Ewma::get10() const
73 {
74 return d_10;
75 }
76
77 double Ewma::get5() const
78 {
79 return d_5;
80 }
81
82 double Ewma::get1() const
83 {
84 return d_1;
85 }
86
87 double Ewma::getMax() const
88 {
89 return d_max;
90 }
91
92 static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp);
93
94 // QTypes that MUST NOT have multiple records of the same type in a given RRset.
95 static const std::set<uint16_t> onlyOneEntryTypes = {QType::CNAME, QType::DNAME, QType::SOA};
96 // QTypes that MUST NOT be used with any other QType on the same name.
97 static const std::set<uint16_t> exclusiveEntryTypes = {QType::CNAME};
98 // QTypes that MUST be at apex.
99 static const std::set<uint16_t> atApexTypes = {QType::SOA, QType::DNSKEY};
100 // QTypes that are NOT allowed at apex.
101 static const std::set<uint16_t> nonApexTypes = {QType::DS};
102
103 AuthWebServer::AuthWebServer() :
104 d_start(time(nullptr)),
105 d_min10(0),
106 d_min5(0),
107 d_min1(0)
108 {
109 if (arg().mustDo("webserver") || arg().mustDo("api")) {
110 d_ws = std::make_unique<WebServer>(arg()["webserver-address"], arg().asNum("webserver-port"));
111 d_ws->setApiKey(arg()["api-key"], arg().mustDo("webserver-hash-plaintext-credentials"));
112 d_ws->setPassword(arg()["webserver-password"], arg().mustDo("webserver-hash-plaintext-credentials"));
113 d_ws->setLogLevel(arg()["webserver-loglevel"]);
114
115 NetmaskGroup acl;
116 acl.toMasks(::arg()["webserver-allow-from"]);
117 d_ws->setACL(acl);
118
119 d_ws->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
120
121 d_ws->bind();
122 }
123 }
124
125 void AuthWebServer::go(StatBag& stats)
126 {
127 S.doRings();
128 std::thread webT([this]() { webThread(); });
129 webT.detach();
130 std::thread statT([this, &stats]() { statThread(stats); });
131 statT.detach();
132 }
133
134 void AuthWebServer::statThread(StatBag& stats)
135 {
136 try {
137 setThreadName("pdns/statHelper");
138 for (;;) {
139 d_queries.submit(static_cast<int>(stats.read("udp-queries")));
140 d_cachehits.submit(static_cast<int>(stats.read("packetcache-hit")));
141 d_cachemisses.submit(static_cast<int>(stats.read("packetcache-miss")));
142 d_qcachehits.submit(static_cast<int>(stats.read("query-cache-hit")));
143 d_qcachemisses.submit(static_cast<int>(stats.read("query-cache-miss")));
144 Utility::sleep(1);
145 }
146 }
147 catch (...) {
148 g_log << Logger::Error << "Webserver statThread caught an exception, dying" << endl;
149 _exit(1);
150 }
151 }
152
153 static string htmlescape(const string& inputString)
154 {
155 string result;
156 for (char currentChar : inputString) {
157 switch (currentChar) {
158 case '&':
159 result += "&amp;";
160 break;
161 case '<':
162 result += "&lt;";
163 break;
164 case '>':
165 result += "&gt;";
166 break;
167 case '"':
168 result += "&quot;";
169 break;
170 default:
171 result += currentChar;
172 }
173 }
174 return result;
175 }
176
177 static void printtable(ostringstream& ret, const string& ringname, const string& title, int limit = 10)
178 {
179 unsigned int tot = 0;
180 int entries = 0;
181 vector<pair<string, unsigned int>> ring = S.getRing(ringname);
182
183 for (const auto& entry : ring) {
184 tot += entry.second;
185 entries++;
186 }
187
188 ret << "<div class=\"panel\">";
189 ret << "<span class=resetring><i></i><a href=\"?resetring=" << htmlescape(ringname) << "\">Reset</a></span>" << endl;
190 ret << "<h2>" << title << "</h2>" << endl;
191 ret << "<div class=ringmeta>";
192 ret << "<a class=topXofY href=\"?ring=" << htmlescape(ringname) << "\">Showing: Top " << limit << " of " << entries << "</a>" << endl;
193 ret << "<span class=resizering>Resize: ";
194 std::vector<uint64_t> sizes{10, 100, 500, 1000, 10000, 500000, 0};
195 for (int i = 0; sizes[i] != 0; ++i) {
196 if (S.getRingSize(ringname) != sizes[i]) {
197 ret << "<a href=\"?resizering=" << htmlescape(ringname) << "&amp;size=" << sizes[i] << "\">" << sizes[i] << "</a> ";
198 }
199 else {
200 ret << "(" << sizes[i] << ") ";
201 }
202 }
203 ret << "</span></div>";
204
205 ret << "<table class=\"data\">";
206 unsigned int printed = 0;
207 unsigned int total = std::max(1U, tot);
208 for (auto i = ring.begin(); limit != 0 && i != ring.end(); ++i, --limit) {
209 ret << "<tr><td>" << htmlescape(i->first) << "</td><td>" << i->second << "</td><td align=right>" << AuthWebServer::makePercentage(i->second * 100.0 / total) << "</td>" << endl;
210 printed += i->second;
211 }
212 ret << "<tr><td colspan=3></td></tr>" << endl;
213 if (printed != tot) {
214 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;
215 }
216
217 ret << "<tr><td><b>Total:</b></td><td><b>" << tot << "</b></td><td align=right><b>100%</b></td>";
218 ret << "</table></div>" << endl;
219 }
220
221 static void printvars(ostringstream& ret)
222 {
223 ret << "<div class=panel><h2>Variables</h2><table class=\"data\">" << endl;
224
225 vector<string> entries = S.getEntries();
226 for (const auto& entry : entries) {
227 ret << "<tr><td>" << entry << "</td><td>" << S.read(entry) << "</td><td>" << S.getDescrip(entry) << "</td>" << endl;
228 }
229
230 ret << "</table></div>" << endl;
231 }
232
233 static void printargs(ostringstream& ret)
234 {
235 ret << R"(<table border=1><tr><td colspan=3 bgcolor="#0000ff"><font color="#ffffff">Arguments</font></td>)" << endl;
236
237 vector<string> entries = arg().list();
238 for (const auto& entry : entries) {
239 ret << "<tr><td>" << entry << "</td><td>" << arg()[entry] << "</td><td>" << arg().getHelp(entry) << "</td>" << endl;
240 }
241 }
242
243 string AuthWebServer::makePercentage(const double& val)
244 {
245 return (boost::format("%.01f%%") % val).str();
246 }
247
248 void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
249 {
250 if (!req->getvars["resetring"].empty()) {
251 if (S.ringExists(req->getvars["resetring"])) {
252 S.resetRing(req->getvars["resetring"]);
253 }
254 resp->status = 302;
255 resp->headers["Location"] = req->url.path;
256 return;
257 }
258 if (!req->getvars["resizering"].empty()) {
259 int size = std::stoi(req->getvars["size"]);
260 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000) {
261 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
262 }
263 resp->status = 302;
264 resp->headers["Location"] = req->url.path;
265 return;
266 }
267
268 ostringstream ret;
269
270 ret << "<!DOCTYPE html>" << endl;
271 ret << "<html><head>" << endl;
272 ret << "<title>PowerDNS Authoritative Server Monitor</title>" << endl;
273 ret << R"(<link rel="stylesheet" href="style.css"/>)" << endl;
274 ret << "</head><body>" << endl;
275
276 ret << "<div class=\"row\">" << endl;
277 ret << "<div class=\"headl columns\">";
278 ret << R"(<a href="/" id="appname">PowerDNS )" << htmlescape(VERSION);
279 if (!arg()["config-name"].empty()) {
280 ret << " [" << htmlescape(arg()["config-name"]) << "]";
281 }
282 ret << "</a></div>" << endl;
283 ret << "<div class=\"header columns\"></div></div>";
284 ret << R"(<div class="row"><div class="all columns">)";
285
286 time_t passed = time(nullptr) - g_starttime;
287
288 ret << "<p>Uptime: " << humanDuration(passed) << "<br>" << endl;
289
290 ret << "Queries/second, 1, 5, 10 minute averages: " << std::setprecision(3) << (int)d_queries.get1() << ", " << (int)d_queries.get5() << ", " << (int)d_queries.get10() << ". Max queries/second: " << (int)d_queries.getMax() << "<br>" << endl;
291
292 if (d_cachemisses.get10() + d_cachehits.get10() > 0) {
293 ret << "Cache hitrate, 1, 5, 10 minute averages: " << makePercentage((d_cachehits.get1() * 100.0) / ((d_cachehits.get1()) + (d_cachemisses.get1()))) << ", " << makePercentage((d_cachehits.get5() * 100.0) / ((d_cachehits.get5()) + (d_cachemisses.get5()))) << ", " << makePercentage((d_cachehits.get10() * 100.0) / ((d_cachehits.get10()) + (d_cachemisses.get10()))) << "<br>" << endl;
294 }
295
296 if (d_qcachemisses.get10() + d_qcachehits.get10() > 0) {
297 ret << "Backend query cache hitrate, 1, 5, 10 minute averages: " << std::setprecision(2) << makePercentage((d_qcachehits.get1() * 100.0) / ((d_qcachehits.get1()) + (d_qcachemisses.get1()))) << ", " << makePercentage((d_qcachehits.get5() * 100.0) / ((d_qcachehits.get5()) + (d_qcachemisses.get5()))) << ", " << makePercentage((d_qcachehits.get10() * 100.0) / ((d_qcachehits.get10()) + (d_qcachemisses.get10()))) << "<br>" << endl;
298 }
299
300 ret << "Backend query load, 1, 5, 10 minute averages: " << std::setprecision(3) << (int)d_qcachemisses.get1() << ", " << (int)d_qcachemisses.get5() << ", " << (int)d_qcachemisses.get10() << ". Max queries/second: " << (int)d_qcachemisses.getMax() << "<br>" << endl;
301
302 ret << "Total queries: " << S.read("udp-queries") << ". Question/answer latency: " << static_cast<double>(S.read("latency")) / 1000.0 << "ms</p><br>" << endl;
303 if (req->getvars["ring"].empty()) {
304 auto entries = S.listRings();
305 for (const auto& entry : entries) {
306 printtable(ret, entry, S.getRingTitle(entry));
307 }
308
309 printvars(ret);
310 if (arg().mustDo("webserver-print-arguments")) {
311 printargs(ret);
312 }
313 }
314 else if (S.ringExists(req->getvars["ring"])) {
315 printtable(ret, req->getvars["ring"], S.getRingTitle(req->getvars["ring"]), 100);
316 }
317
318 ret << "</div></div>" << endl;
319 ret << "<footer class=\"row\">" << fullVersionString() << "<br>&copy; <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>" << endl;
320 ret << "</body></html>" << endl;
321
322 resp->body = ret.str();
323 resp->status = 200;
324 }
325
326 /** Helper to build a record content as needed. */
327 static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot)
328 {
329 // noDot: for backend storage, pass true. for API users, pass false.
330 auto drc = DNSRecordContent::make(qtype.getCode(), QClass::IN, content);
331 return drc->getZoneRepresentation(noDot);
332 }
333
334 /** "Normalize" record content for API consumers. */
335 static inline string makeApiRecordContent(const QType& qtype, const string& content)
336 {
337 return makeRecordContent(qtype, content, false);
338 }
339
340 /** "Normalize" record content for backend storage. */
341 static inline string makeBackendRecordContent(const QType& qtype, const string& content)
342 {
343 return makeRecordContent(qtype, content, true);
344 }
345
346 static Json::object getZoneInfo(const DomainInfo& domainInfo, DNSSECKeeper* dnssecKeeper)
347 {
348 string zoneId = apiZoneNameToId(domainInfo.zone);
349 vector<string> primaries;
350 primaries.reserve(domainInfo.primaries.size());
351 for (const auto& primary : domainInfo.primaries) {
352 primaries.push_back(primary.toStringWithPortExcept(53));
353 }
354
355 auto obj = Json::object{
356 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
357 {"id", zoneId},
358 {"url", "/api/v1/servers/localhost/zones/" + zoneId},
359 {"name", domainInfo.zone.toString()},
360 {"kind", domainInfo.getKindString()},
361 {"catalog", (!domainInfo.catalog.empty() ? domainInfo.catalog.toString() : "")},
362 {"account", domainInfo.account},
363 {"masters", std::move(primaries)},
364 {"serial", (double)domainInfo.serial},
365 {"notified_serial", (double)domainInfo.notified_serial},
366 {"last_check", (double)domainInfo.last_check}};
367 if (dnssecKeeper != nullptr) {
368 obj["dnssec"] = dnssecKeeper->isSecuredZone(domainInfo.zone);
369 string soa_edit;
370 dnssecKeeper->getSoaEdit(domainInfo.zone, soa_edit, false);
371 obj["edited_serial"] = (double)calculateEditSOA(domainInfo.serial, soa_edit, domainInfo.zone);
372 }
373 return obj;
374 }
375
376 static bool shouldDoRRSets(HttpRequest* req)
377 {
378 if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true") {
379 return true;
380 }
381 if (req->getvars["rrsets"] == "false") {
382 return false;
383 }
384
385 throw ApiException("'rrsets' request parameter value '" + req->getvars["rrsets"] + "' is not supported");
386 }
387
388 static void fillZone(UeberBackend& backend, const DNSName& zonename, HttpResponse* resp, HttpRequest* req)
389 {
390 DomainInfo domainInfo;
391
392 if (!backend.getDomainInfo(zonename, domainInfo)) {
393 throw HttpNotFoundException();
394 }
395
396 DNSSECKeeper dnssecKeeper(&backend);
397 Json::object doc = getZoneInfo(domainInfo, &dnssecKeeper);
398 // extra stuff getZoneInfo doesn't do for us (more expensive)
399 string soa_edit_api;
400 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
401 doc["soa_edit_api"] = soa_edit_api;
402 string soa_edit;
403 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
404 doc["soa_edit"] = soa_edit;
405
406 string nsec3param;
407 bool nsec3narrowbool = false;
408 bool is_secured = dnssecKeeper.isSecuredZone(zonename);
409 if (is_secured) { // ignore NSEC3PARAM and NSEC3NARROW metadata present in the db for unsigned zones
410 domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
411 string nsec3narrow;
412 domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
413 if (nsec3narrow == "1") {
414 nsec3narrowbool = true;
415 }
416 }
417 doc["nsec3param"] = nsec3param;
418 doc["nsec3narrow"] = nsec3narrowbool;
419 doc["dnssec"] = is_secured;
420
421 string api_rectify;
422 domainInfo.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
423 doc["api_rectify"] = (api_rectify == "1");
424
425 // TSIG
426 vector<string> tsig_primary;
427 vector<string> tsig_secondary;
428 domainInfo.backend->getDomainMetadata(zonename, "TSIG-ALLOW-AXFR", tsig_primary);
429 domainInfo.backend->getDomainMetadata(zonename, "AXFR-MASTER-TSIG", tsig_secondary);
430
431 Json::array tsig_primary_keys;
432 for (const auto& keyname : tsig_primary) {
433 tsig_primary_keys.emplace_back(apiZoneNameToId(DNSName(keyname)));
434 }
435 doc["master_tsig_key_ids"] = tsig_primary_keys;
436
437 Json::array tsig_secondary_keys;
438 for (const auto& keyname : tsig_secondary) {
439 tsig_secondary_keys.emplace_back(apiZoneNameToId(DNSName(keyname)));
440 }
441 doc["slave_tsig_key_ids"] = tsig_secondary_keys;
442
443 if (shouldDoRRSets(req)) {
444 vector<DNSResourceRecord> records;
445 vector<Comment> comments;
446
447 // load all records + sort
448 {
449 DNSResourceRecord resourceRecord;
450 if (req->getvars.count("rrset_name") == 0) {
451 domainInfo.backend->list(zonename, static_cast<int>(domainInfo.id), true); // incl. disabled
452 }
453 else {
454 QType qType;
455 if (req->getvars.count("rrset_type") == 0) {
456 qType = QType::ANY;
457 }
458 else {
459 qType = req->getvars["rrset_type"];
460 }
461 domainInfo.backend->lookup(qType, DNSName(req->getvars["rrset_name"]), static_cast<int>(domainInfo.id));
462 }
463 while (domainInfo.backend->get(resourceRecord)) {
464 if (resourceRecord.qtype.getCode() == 0) {
465 continue; // skip empty non-terminals
466 }
467 records.push_back(resourceRecord);
468 }
469 sort(records.begin(), records.end(), [](const DNSResourceRecord& rrA, const DNSResourceRecord& rrB) {
470 /* if you ever want to update this comparison function,
471 please be aware that you will also need to update the conditions in the code merging
472 the records and comments below */
473 if (rrA.qname == rrB.qname) {
474 return rrB.qtype < rrA.qtype;
475 }
476 return rrB.qname < rrA.qname;
477 });
478 }
479
480 // load all comments + sort
481 {
482 Comment comment;
483 domainInfo.backend->listComments(domainInfo.id);
484 while (domainInfo.backend->getComment(comment)) {
485 comments.push_back(comment);
486 }
487 sort(comments.begin(), comments.end(), [](const Comment& rrA, const Comment& rrB) {
488 /* if you ever want to update this comparison function,
489 please be aware that you will also need to update the conditions in the code merging
490 the records and comments below */
491 if (rrA.qname == rrB.qname) {
492 return rrB.qtype < rrA.qtype;
493 }
494 return rrB.qname < rrA.qname;
495 });
496 }
497
498 Json::array rrsets;
499 Json::object rrset;
500 Json::array rrset_records;
501 Json::array rrset_comments;
502 DNSName current_qname;
503 QType current_qtype;
504 uint32_t ttl = 0;
505 auto rit = records.begin();
506 auto cit = comments.begin();
507
508 while (rit != records.end() || cit != comments.end()) {
509 // 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
510 if (cit == comments.end() || (rit != records.end() && (rit->qname == cit->qname ? (cit->qtype < rit->qtype || cit->qtype == rit->qtype) : cit->qname < rit->qname))) {
511 current_qname = rit->qname;
512 current_qtype = rit->qtype;
513 ttl = rit->ttl;
514 }
515 else {
516 current_qname = cit->qname;
517 current_qtype = cit->qtype;
518 ttl = 0;
519 }
520
521 while (rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
522 ttl = min(ttl, rit->ttl);
523 rrset_records.push_back(Json::object{
524 {"disabled", rit->disabled},
525 {"content", makeApiRecordContent(rit->qtype, rit->content)}});
526 rit++;
527 }
528 while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
529 rrset_comments.push_back(Json::object{
530 {"modified_at", (double)cit->modified_at},
531 {"account", cit->account},
532 {"content", cit->content}});
533 cit++;
534 }
535
536 rrset["name"] = current_qname.toString();
537 rrset["type"] = current_qtype.toString();
538 rrset["records"] = rrset_records;
539 rrset["comments"] = rrset_comments;
540 rrset["ttl"] = (double)ttl;
541 rrsets.emplace_back(rrset);
542 rrset.clear();
543 rrset_records.clear();
544 rrset_comments.clear();
545 }
546
547 doc["rrsets"] = rrsets;
548 }
549
550 resp->setJsonBody(doc);
551 }
552
553 void productServerStatisticsFetch(map<string, string>& out)
554 {
555 vector<string> items = S.getEntries();
556 for (const string& item : items) {
557 out[item] = std::to_string(S.read(item));
558 }
559
560 // add uptime
561 out["uptime"] = std::to_string(time(nullptr) - g_starttime);
562 }
563
564 std::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
565 {
566 try {
567 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
568 return S.read(name);
569 }
570 catch (...) {
571 return std::nullopt;
572 }
573 }
574
575 static void validateGatheredRRType(const DNSResourceRecord& resourceRecord)
576 {
577 if (resourceRecord.qtype.getCode() == QType::OPT || resourceRecord.qtype.getCode() == QType::TSIG) {
578 throw ApiException("RRset " + resourceRecord.qname.toString() + " IN " + resourceRecord.qtype.toString() + ": invalid type given");
579 }
580 }
581
582 static void gatherRecords(const Json& container, const DNSName& qname, const QType& qtype, const uint32_t ttl, vector<DNSResourceRecord>& new_records)
583 {
584 DNSResourceRecord resourceRecord;
585 resourceRecord.qname = qname;
586 resourceRecord.qtype = qtype;
587 resourceRecord.auth = true;
588 resourceRecord.ttl = ttl;
589
590 validateGatheredRRType(resourceRecord);
591 const auto& items = container["records"].array_items();
592 for (const auto& record : items) {
593 string content = stringFromJson(record, "content");
594 if (record.object_items().count("priority") > 0) {
595 throw std::runtime_error("`priority` element is not allowed in record");
596 }
597 resourceRecord.disabled = false;
598 if (!record["disabled"].is_null()) {
599 resourceRecord.disabled = boolFromJson(record, "disabled");
600 }
601
602 // validate that the client sent something we can actually parse, and require that data to be dotted.
603 try {
604 if (resourceRecord.qtype.getCode() != QType::AAAA) {
605 string tmp = makeApiRecordContent(resourceRecord.qtype, content);
606 if (!pdns_iequals(tmp, content)) {
607 throw std::runtime_error("Not in expected format (parsed as '" + tmp + "')");
608 }
609 }
610 else {
611 struct in6_addr tmpbuf
612 {
613 };
614 if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
615 throw std::runtime_error("Invalid IPv6 address");
616 }
617 }
618 resourceRecord.content = makeBackendRecordContent(resourceRecord.qtype, content);
619 }
620 catch (std::exception& e) {
621 throw ApiException("Record " + resourceRecord.qname.toString() + "/" + resourceRecord.qtype.toString() + " '" + content + "': " + e.what());
622 }
623
624 new_records.push_back(resourceRecord);
625 }
626 }
627
628 static void gatherComments(const Json& container, const DNSName& qname, const QType& qtype, vector<Comment>& new_comments)
629 {
630 Comment comment;
631 comment.qname = qname;
632 comment.qtype = qtype;
633
634 time_t now = time(nullptr);
635 for (const auto& currentComment : container["comments"].array_items()) {
636 // FIXME 2036 issue internally in uintFromJson
637 comment.modified_at = uintFromJson(currentComment, "modified_at", now);
638 comment.content = stringFromJson(currentComment, "content");
639 comment.account = stringFromJson(currentComment, "account");
640 new_comments.push_back(comment);
641 }
642 }
643
644 static void checkDefaultDNSSECAlgos()
645 {
646 int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
647 int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
648 int k_size = arg().asNum("default-ksk-size");
649 int z_size = arg().asNum("default-zsk-size");
650
651 // Sanity check DNSSEC parameters
652 if (!::arg()["default-zsk-algorithm"].empty()) {
653 if (k_algo == -1) {
654 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
655 }
656 if (k_algo <= 10 && k_size == 0) {
657 throw ApiException("default-ksk-algorithm is set to an algorithm(" + ::arg()["default-ksk-algorithm"] + ") that requires a non-zero default-ksk-size!");
658 }
659 }
660
661 if (!::arg()["default-zsk-algorithm"].empty()) {
662 if (z_algo == -1) {
663 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
664 }
665 if (z_algo <= 10 && z_size == 0) {
666 throw ApiException("default-zsk-algorithm is set to an algorithm(" + ::arg()["default-zsk-algorithm"] + ") that requires a non-zero default-zsk-size!");
667 }
668 }
669 }
670
671 static void throwUnableToSecure(const DNSName& zonename)
672 {
673 throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
674 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
675 }
676
677 /*
678 * Add KSK and ZSK to an existing zone. Algorithms and sizes will be chosen per configuration.
679 */
680 static void addDefaultDNSSECKeys(DNSSECKeeper& dnssecKeeper, const DNSName& zonename)
681 {
682 checkDefaultDNSSECAlgos();
683 int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
684 int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
685 int k_size = arg().asNum("default-ksk-size");
686 int z_size = arg().asNum("default-zsk-size");
687
688 if (k_algo != -1) {
689 int64_t keyID{-1};
690 if (!dnssecKeeper.addKey(zonename, true, k_algo, keyID, k_size)) {
691 throwUnableToSecure(zonename);
692 }
693 }
694
695 if (z_algo != -1) {
696 int64_t keyID{-1};
697 if (!dnssecKeeper.addKey(zonename, false, z_algo, keyID, z_size)) {
698 throwUnableToSecure(zonename);
699 }
700 }
701 }
702
703 static bool isZoneApiRectifyEnabled(const DomainInfo& domainInfo)
704 {
705 string api_rectify;
706 domainInfo.backend->getDomainMetadataOne(domainInfo.zone, "API-RECTIFY", api_rectify);
707 if (api_rectify.empty() && ::arg().mustDo("default-api-rectify")) {
708 api_rectify = "1";
709 }
710 return api_rectify == "1";
711 }
712
713 static void extractDomainInfoFromDocument(const Json& document, boost::optional<DomainInfo::DomainKind>& kind, boost::optional<vector<ComboAddress>>& primaries, boost::optional<DNSName>& catalog, boost::optional<string>& account)
714 {
715 if (document["kind"].is_string()) {
716 kind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
717 }
718 else {
719 kind = boost::none;
720 }
721
722 if (document["masters"].is_array()) {
723 primaries = vector<ComboAddress>();
724 for (const auto& value : document["masters"].array_items()) {
725 string primary = value.string_value();
726 if (primary.empty()) {
727 throw ApiException("Primary can not be an empty string");
728 }
729 try {
730 primaries->emplace_back(primary, 53);
731 }
732 catch (const PDNSException& e) {
733 throw ApiException("Primary (" + primary + ") is not an IP address: " + e.reason);
734 }
735 }
736 }
737 else {
738 primaries = boost::none;
739 }
740
741 if (document["catalog"].is_string()) {
742 string catstring = document["catalog"].string_value();
743 catalog = (!catstring.empty() ? DNSName(catstring) : DNSName());
744 }
745 else {
746 catalog = boost::none;
747 }
748
749 if (document["account"].is_string()) {
750 account = document["account"].string_value();
751 }
752 else {
753 account = boost::none;
754 }
755 }
756
757 /*
758 * Build vector of TSIG Key ids from domain update document.
759 * jsonArray: JSON array element to extract TSIG key ids from.
760 * metadata: returned list of domain key ids for setDomainMetadata
761 */
762 static void extractJsonTSIGKeyIds(UeberBackend& backend, const Json& jsonArray, vector<string>& metadata)
763 {
764 for (const auto& value : jsonArray.array_items()) {
765 auto keyname(apiZoneIdToName(value.string_value()));
766 DNSName keyAlgo;
767 string keyContent;
768 if (!backend.getTSIGKey(keyname, keyAlgo, keyContent)) {
769 throw ApiException("A TSIG key with the name '" + keyname.toLogString() + "' does not exist");
770 }
771 metadata.push_back(keyname.toString());
772 }
773 }
774
775 // Must be called within backend transaction.
776 static void updateDomainSettingsFromDocument(UeberBackend& backend, DomainInfo& domainInfo, const DNSName& zonename, const Json& document, bool zoneWasModified)
777 {
778 boost::optional<DomainInfo::DomainKind> kind;
779 boost::optional<vector<ComboAddress>> primaries;
780 boost::optional<DNSName> catalog;
781 boost::optional<string> account;
782
783 extractDomainInfoFromDocument(document, kind, primaries, catalog, account);
784
785 if (kind) {
786 domainInfo.backend->setKind(zonename, *kind);
787 domainInfo.kind = *kind;
788 }
789 if (primaries) {
790 domainInfo.backend->setPrimaries(zonename, *primaries);
791 }
792 if (catalog) {
793 domainInfo.backend->setCatalog(zonename, *catalog);
794 }
795 if (account) {
796 domainInfo.backend->setAccount(zonename, *account);
797 }
798
799 if (document["soa_edit_api"].is_string()) {
800 domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
801 }
802 if (document["soa_edit"].is_string()) {
803 domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
804 }
805 try {
806 bool api_rectify = boolFromJson(document, "api_rectify");
807 domainInfo.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0");
808 }
809 catch (const JsonException&) {
810 }
811
812 DNSSECKeeper dnssecKeeper(&backend);
813 bool shouldRectify = zoneWasModified;
814 bool dnssecInJSON = false;
815 bool dnssecDocVal = false;
816 bool nsec3paramInJSON = false;
817 bool updateNsec3Param = false;
818 string nsec3paramDocVal;
819
820 try {
821 dnssecDocVal = boolFromJson(document, "dnssec");
822 dnssecInJSON = true;
823 }
824 catch (const JsonException&) {
825 }
826
827 try {
828 nsec3paramDocVal = stringFromJson(document, "nsec3param");
829 nsec3paramInJSON = true;
830 }
831 catch (const JsonException&) {
832 }
833
834 bool isDNSSECZone = dnssecKeeper.isSecuredZone(zonename);
835 bool isPresigned = dnssecKeeper.isPresigned(zonename);
836
837 if (dnssecInJSON) {
838 if (dnssecDocVal) {
839 if (!isDNSSECZone) {
840 addDefaultDNSSECKeys(dnssecKeeper, zonename);
841
842 // Used later for NSEC3PARAM
843 isDNSSECZone = dnssecKeeper.isSecuredZone(zonename);
844
845 if (!isDNSSECZone) {
846 throwUnableToSecure(zonename);
847 }
848 shouldRectify = true;
849 updateNsec3Param = true;
850 }
851 }
852 else {
853 // "dnssec": false in json
854 if (isDNSSECZone) {
855 string info;
856 string error;
857 if (!dnssecKeeper.unSecureZone(zonename, error)) {
858 throw ApiException("Error while un-securing zone '" + zonename.toString() + "': " + error);
859 }
860 isDNSSECZone = dnssecKeeper.isSecuredZone(zonename, false);
861 if (isDNSSECZone) {
862 throw ApiException("Unable to un-secure zone '" + zonename.toString() + "'");
863 }
864 shouldRectify = true;
865 updateNsec3Param = true;
866 }
867 }
868 }
869
870 if (nsec3paramInJSON || updateNsec3Param) {
871 shouldRectify = true;
872 if (!isDNSSECZone && !nsec3paramDocVal.empty()) {
873 throw ApiException("NSEC3PARAM value provided for zone '" + zonename.toString() + "', but zone is not DNSSEC secured.");
874 }
875
876 if (nsec3paramDocVal.empty()) {
877 // Switch to NSEC
878 if (!dnssecKeeper.unsetNSEC3PARAM(zonename)) {
879 throw ApiException("Unable to remove NSEC3PARAMs from zone '" + zonename.toString());
880 }
881 }
882 else {
883 // Set the NSEC3PARAMs
884 NSEC3PARAMRecordContent ns3pr(nsec3paramDocVal);
885 string error_msg;
886 if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) {
887 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg);
888 }
889 if (!dnssecKeeper.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
890 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' passed our basic sanity checks, but cannot be used with the current backend.");
891 }
892 }
893 }
894
895 if (shouldRectify && !isPresigned) {
896 // Rectify
897 if (isZoneApiRectifyEnabled(domainInfo)) {
898 string info;
899 string error_msg;
900 if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false) && !domainInfo.isSecondaryType()) {
901 // for Secondary zones, it is possible that rectifying was not needed (example: empty zone).
902 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
903 }
904 }
905
906 // Increase serial
907 string soa_edit_api_kind;
908 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
909 if (!soa_edit_api_kind.empty()) {
910 SOAData soaData;
911 if (!backend.getSOAUncached(zonename, soaData)) {
912 return;
913 }
914
915 string soa_edit_kind;
916 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
917
918 DNSResourceRecord resourceRecord;
919 if (makeIncreasedSOARecord(soaData, soa_edit_api_kind, soa_edit_kind, resourceRecord)) {
920 if (!domainInfo.backend->replaceRRSet(domainInfo.id, resourceRecord.qname, resourceRecord.qtype, vector<DNSResourceRecord>(1, resourceRecord))) {
921 throw ApiException("Hosting backend does not support editing records.");
922 }
923 }
924 }
925 }
926
927 if (!document["master_tsig_key_ids"].is_null()) {
928 vector<string> metadata;
929 extractJsonTSIGKeyIds(backend, document["master_tsig_key_ids"], metadata);
930 if (!domainInfo.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) {
931 throw HttpInternalServerErrorException("Unable to set new TSIG primary keys for zone '" + zonename.toLogString() + "'");
932 }
933 }
934 if (!document["slave_tsig_key_ids"].is_null()) {
935 vector<string> metadata;
936 extractJsonTSIGKeyIds(backend, document["slave_tsig_key_ids"], metadata);
937 if (!domainInfo.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) {
938 throw HttpInternalServerErrorException("Unable to set new TSIG secondary keys for zone '" + zonename.toLogString() + "'");
939 }
940 }
941 }
942
943 static bool isValidMetadataKind(const string& kind, bool readonly)
944 {
945 static vector<string> builtinOptions{
946 "ALLOW-AXFR-FROM",
947 "AXFR-SOURCE",
948 "ALLOW-DNSUPDATE-FROM",
949 "TSIG-ALLOW-DNSUPDATE",
950 "FORWARD-DNSUPDATE",
951 "SOA-EDIT-DNSUPDATE",
952 "NOTIFY-DNSUPDATE",
953 "ALSO-NOTIFY",
954 "AXFR-MASTER-TSIG",
955 "GSS-ALLOW-AXFR-PRINCIPAL",
956 "GSS-ACCEPTOR-PRINCIPAL",
957 "IXFR",
958 "LUA-AXFR-SCRIPT",
959 "NSEC3NARROW",
960 "NSEC3PARAM",
961 "PRESIGNED",
962 "PUBLISH-CDNSKEY",
963 "PUBLISH-CDS",
964 "SLAVE-RENOTIFY",
965 "SOA-EDIT",
966 "TSIG-ALLOW-AXFR",
967 "TSIG-ALLOW-DNSUPDATE",
968 };
969
970 // the following options do not allow modifications via API
971 static vector<string> protectedOptions{
972 "API-RECTIFY",
973 "AXFR-MASTER-TSIG",
974 "NSEC3NARROW",
975 "NSEC3PARAM",
976 "PRESIGNED",
977 "LUA-AXFR-SCRIPT",
978 "TSIG-ALLOW-AXFR",
979 };
980
981 if (kind.find("X-") == 0) {
982 return true;
983 }
984
985 bool found = false;
986
987 for (const string& builtinOption : builtinOptions) {
988 if (kind == builtinOption) {
989 for (const string& protectedOption : protectedOptions) {
990 if (!readonly && builtinOption == protectedOption) {
991 return false;
992 }
993 }
994 found = true;
995 break;
996 }
997 }
998
999 return found;
1000 }
1001
1002 /* Return OpenAPI document describing the supported API.
1003 */
1004 #include "apidocfiles.h"
1005
1006 void apiDocs(HttpRequest* req, HttpResponse* resp)
1007 {
1008 if (req->accept_yaml) {
1009 resp->setYamlBody(g_api_swagger_yaml);
1010 }
1011 else if (req->accept_json) {
1012 resp->setJsonBody(g_api_swagger_json);
1013 }
1014 else {
1015 resp->setPlainBody(g_api_swagger_yaml);
1016 }
1017 }
1018
1019 class ZoneData
1020 {
1021 public:
1022 ZoneData(HttpRequest* req) :
1023 zoneName(apiZoneIdToName((req)->parameters["id"])),
1024 dnssecKeeper(DNSSECKeeper{&backend})
1025 {
1026 try {
1027 if (!backend.getDomainInfo(zoneName, domainInfo)) {
1028 throw HttpNotFoundException();
1029 }
1030 }
1031 catch (const PDNSException& e) {
1032 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason);
1033 }
1034 }
1035
1036 DNSName zoneName;
1037 UeberBackend backend{};
1038 DNSSECKeeper dnssecKeeper;
1039 DomainInfo domainInfo{};
1040 };
1041
1042 static void apiZoneMetadataGET(HttpRequest* req, HttpResponse* resp)
1043 {
1044 ZoneData zoneData{req};
1045
1046 map<string, vector<string>> metas;
1047 Json::array document;
1048
1049 if (!zoneData.backend.getAllDomainMetadata(zoneData.zoneName, metas)) {
1050 throw HttpNotFoundException();
1051 }
1052
1053 for (const auto& meta : metas) {
1054 Json::array entries;
1055 for (const string& value : meta.second) {
1056 entries.emplace_back(value);
1057 }
1058
1059 Json::object key{
1060 {"type", "Metadata"},
1061 {"kind", meta.first},
1062 {"metadata", entries}};
1063 document.emplace_back(key);
1064 }
1065 resp->setJsonBody(document);
1066 }
1067
1068 static void apiZoneMetadataPOST(HttpRequest* req, HttpResponse* resp)
1069 {
1070 ZoneData zoneData{req};
1071
1072 const auto& document = req->json();
1073 string kind;
1074 vector<string> entries;
1075
1076 try {
1077 kind = stringFromJson(document, "kind");
1078 }
1079 catch (const JsonException&) {
1080 throw ApiException("kind is not specified or not a string");
1081 }
1082
1083 if (!isValidMetadataKind(kind, false)) {
1084 throw ApiException("Unsupported metadata kind '" + kind + "'");
1085 }
1086
1087 vector<string> vecMetadata;
1088
1089 if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
1090 throw ApiException("Could not retrieve metadata entries for domain '" + zoneData.zoneName.toString() + "'");
1091 }
1092
1093 const auto& metadata = document["metadata"];
1094 if (!metadata.is_array()) {
1095 throw ApiException("metadata is not specified or not an array");
1096 }
1097
1098 for (const auto& value : metadata.array_items()) {
1099 if (!value.is_string()) {
1100 throw ApiException("metadata must be strings");
1101 }
1102 if (std::find(vecMetadata.cbegin(),
1103 vecMetadata.cend(),
1104 value.string_value())
1105 == vecMetadata.cend()) {
1106 vecMetadata.push_back(value.string_value());
1107 }
1108 }
1109
1110 if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
1111 throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'");
1112 }
1113
1114 DNSSECKeeper::clearMetaCache(zoneData.zoneName);
1115
1116 Json::array respMetadata;
1117 for (const string& value : vecMetadata) {
1118 respMetadata.emplace_back(value);
1119 }
1120
1121 Json::object key{
1122 {"type", "Metadata"},
1123 {"kind", document["kind"]},
1124 {"metadata", respMetadata}};
1125
1126 resp->status = 201;
1127 resp->setJsonBody(key);
1128 }
1129
1130 static void apiZoneMetadataKindGET(HttpRequest* req, HttpResponse* resp)
1131 {
1132 ZoneData zoneData{req};
1133
1134 string kind = req->parameters["kind"];
1135
1136 vector<string> metadata;
1137 Json::object document;
1138 Json::array entries;
1139
1140 if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, metadata)) {
1141 throw HttpNotFoundException();
1142 }
1143 if (!isValidMetadataKind(kind, true)) {
1144 throw ApiException("Unsupported metadata kind '" + kind + "'");
1145 }
1146
1147 document["type"] = "Metadata";
1148 document["kind"] = kind;
1149
1150 for (const string& value : metadata) {
1151 entries.emplace_back(value);
1152 }
1153
1154 document["metadata"] = entries;
1155 resp->setJsonBody(document);
1156 }
1157
1158 static void apiZoneMetadataKindPUT(HttpRequest* req, HttpResponse* resp)
1159 {
1160 ZoneData zoneData{req};
1161
1162 string kind = req->parameters["kind"];
1163
1164 const auto& document = req->json();
1165
1166 if (!isValidMetadataKind(kind, false)) {
1167 throw ApiException("Unsupported metadata kind '" + kind + "'");
1168 }
1169
1170 vector<string> vecMetadata;
1171 const auto& metadata = document["metadata"];
1172 if (!metadata.is_array()) {
1173 throw ApiException("metadata is not specified or not an array");
1174 }
1175 for (const auto& value : metadata.array_items()) {
1176 if (!value.is_string()) {
1177 throw ApiException("metadata must be strings");
1178 }
1179 vecMetadata.push_back(value.string_value());
1180 }
1181
1182 if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
1183 throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'");
1184 }
1185
1186 DNSSECKeeper::clearMetaCache(zoneData.zoneName);
1187
1188 Json::object key{
1189 {"type", "Metadata"},
1190 {"kind", kind},
1191 {"metadata", metadata}};
1192
1193 resp->setJsonBody(key);
1194 }
1195
1196 static void apiZoneMetadataKindDELETE(HttpRequest* req, HttpResponse* resp)
1197 {
1198 ZoneData zoneData{req};
1199
1200 const string& kind = req->parameters["kind"];
1201 if (!isValidMetadataKind(kind, false)) {
1202 throw ApiException("Unsupported metadata kind '" + kind + "'");
1203 }
1204
1205 vector<string> metadata; // an empty vector will do it
1206 if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, metadata)) {
1207 throw ApiException("Could not delete metadata for domain '" + zoneData.zoneName.toString() + "' (" + kind + ")");
1208 }
1209
1210 DNSSECKeeper::clearMetaCache(zoneData.zoneName);
1211 resp->status = 204;
1212 }
1213
1214 // Throws 404 if the key with inquireKeyId does not exist
1215 static void apiZoneCryptoKeysCheckKeyExists(const DNSName& zonename, int inquireKeyId, DNSSECKeeper* dnssecKeeper)
1216 {
1217 DNSSECKeeper::keyset_t keyset = dnssecKeeper->getKeys(zonename, false);
1218 bool found = false;
1219 for (const auto& value : keyset) {
1220 if (value.second.id == (unsigned)inquireKeyId) {
1221 found = true;
1222 break;
1223 }
1224 }
1225 if (!found) {
1226 throw HttpNotFoundException();
1227 }
1228 }
1229
1230 static inline int getInquireKeyId(HttpRequest* req, const DNSName& zonename, DNSSECKeeper* dnsseckeeper)
1231 {
1232 int inquireKeyId = -1;
1233 if (req->parameters.count("key_id") == 1) {
1234 inquireKeyId = std::stoi(req->parameters["key_id"]);
1235 apiZoneCryptoKeysCheckKeyExists(zonename, inquireKeyId, dnsseckeeper);
1236 }
1237 return inquireKeyId;
1238 }
1239
1240 static void apiZoneCryptokeysExport(const DNSName& zonename, int64_t inquireKeyId, HttpResponse* resp, DNSSECKeeper* dnssec_dk)
1241 {
1242 DNSSECKeeper::keyset_t keyset = dnssec_dk->getKeys(zonename, false);
1243
1244 bool inquireSingleKey = inquireKeyId >= 0;
1245
1246 Json::array doc;
1247 for (const auto& value : keyset) {
1248 if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
1249 continue;
1250 }
1251
1252 string keyType;
1253 switch (value.second.keyType) {
1254 case DNSSECKeeper::KSK:
1255 keyType = "ksk";
1256 break;
1257 case DNSSECKeeper::ZSK:
1258 keyType = "zsk";
1259 break;
1260 case DNSSECKeeper::CSK:
1261 keyType = "csk";
1262 break;
1263 }
1264
1265 Json::object key{
1266 {"type", "Cryptokey"},
1267 {"id", static_cast<int>(value.second.id)},
1268 {"active", value.second.active},
1269 {"published", value.second.published},
1270 {"keytype", keyType},
1271 {"flags", static_cast<uint16_t>(value.first.getFlags())},
1272 {"dnskey", value.first.getDNSKEY().getZoneRepresentation()},
1273 {"algorithm", DNSSECKeeper::algorithm2name(value.first.getAlgorithm())},
1274 {"bits", value.first.getKey()->getBits()}};
1275
1276 string publishCDS;
1277 dnssec_dk->getPublishCDS(zonename, publishCDS);
1278
1279 vector<string> digestAlgos;
1280 stringtok(digestAlgos, publishCDS, ", ");
1281
1282 std::set<unsigned int> CDSalgos;
1283 for (auto const& digestAlgo : digestAlgos) {
1284 CDSalgos.insert(pdns::checked_stoi<unsigned int>(digestAlgo));
1285 }
1286
1287 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
1288 Json::array cdses;
1289 Json::array dses;
1290 for (const uint8_t keyid : {DNSSECKeeper::DIGEST_SHA1, DNSSECKeeper::DIGEST_SHA256, DNSSECKeeper::DIGEST_GOST, DNSSECKeeper::DIGEST_SHA384}) {
1291 try {
1292 string dsRecordContent = makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation();
1293
1294 dses.emplace_back(dsRecordContent);
1295
1296 if (CDSalgos.count(keyid) != 0) {
1297 cdses.emplace_back(dsRecordContent);
1298 }
1299 }
1300 catch (...) {
1301 }
1302 }
1303
1304 key["ds"] = dses;
1305
1306 if (!cdses.empty()) {
1307 key["cds"] = cdses;
1308 }
1309 }
1310
1311 if (inquireSingleKey) {
1312 key["privatekey"] = value.first.getKey()->convertToISC();
1313 resp->setJsonBody(key);
1314 return;
1315 }
1316 doc.emplace_back(key);
1317 }
1318
1319 if (inquireSingleKey) {
1320 // we came here because we couldn't find the requested key.
1321 throw HttpNotFoundException();
1322 }
1323 resp->setJsonBody(doc);
1324 }
1325
1326 static void apiZoneCryptokeysGET(HttpRequest* req, HttpResponse* resp)
1327 {
1328 ZoneData zoneData{req};
1329 const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
1330
1331 apiZoneCryptokeysExport(zoneData.zoneName, inquireKeyId, resp, &zoneData.dnssecKeeper);
1332 }
1333
1334 /*
1335 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1336 * It deletes a key from :zone_name specified by :cryptokey_id.
1337 * Server Answers:
1338 * Case 1: the backend returns true on removal. This means the key is gone.
1339 * The server returns 204 No Content, no body.
1340 * Case 2: the backend returns false on removal. An error occurred.
1341 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1342 * Case 3: the key or zone does not exist.
1343 * The server returns 404 Not Found
1344 * */
1345 static void apiZoneCryptokeysDELETE(HttpRequest* req, HttpResponse* resp)
1346 {
1347 ZoneData zoneData{req};
1348 const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
1349
1350 if (inquireKeyId == -1) {
1351 throw HttpBadRequestException();
1352 }
1353
1354 if (zoneData.dnssecKeeper.removeKey(zoneData.zoneName, inquireKeyId)) {
1355 resp->body = "";
1356 resp->status = 204;
1357 }
1358 else {
1359 resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422);
1360 }
1361 }
1362
1363 /*
1364 * This method adds a key to a zone by generate it or content parameter.
1365 * Parameter:
1366 * {
1367 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1368 * "keytype" : "ksk|zsk" <string>
1369 * "active" : "true|false" <value>
1370 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1371 * "bits" : number of bits <int>
1372 * }
1373 *
1374 * Response:
1375 * Case 1: keytype isn't ksk|zsk
1376 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1377 * Case 2: 'bits' must be a positive integer value.
1378 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1379 * Case 3: The "algorithm" isn't supported
1380 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1381 * Case 4: Algorithm <= 10 and no bits were passed
1382 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1383 * Case 5: The wrong keysize was passed
1384 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1385 * Case 6: If the server cant guess the keysize
1386 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1387 * Case 7: The key-creation failed
1388 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1389 * Case 8: The key in content has the wrong format
1390 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1391 * Case 9: The wrong combination of fields is submitted
1392 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1393 * Case 10: No content and everything was fine
1394 * The server returns 201 Created and all public data about the new cryptokey
1395 * Case 11: With specified content
1396 * The server returns 201 Created and all public data about the added cryptokey
1397 */
1398
1399 static void apiZoneCryptokeysPOST(HttpRequest* req, HttpResponse* resp)
1400 {
1401 ZoneData zoneData{req};
1402
1403 const auto& document = req->json();
1404 string privatekey_fieldname = "privatekey";
1405 auto privatekey = document["privatekey"];
1406 if (privatekey.is_null()) {
1407 // Fallback to the old "content" behaviour
1408 privatekey = document["content"];
1409 privatekey_fieldname = "content";
1410 }
1411 bool active = boolFromJson(document, "active", false);
1412 bool published = boolFromJson(document, "published", true);
1413 bool keyOrZone = false;
1414
1415 if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") {
1416 keyOrZone = true;
1417 }
1418 else if (stringFromJson(document, "keytype") == "zsk") {
1419 keyOrZone = false;
1420 }
1421 else {
1422 throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
1423 }
1424
1425 int64_t insertedId = -1;
1426
1427 if (privatekey.is_null()) {
1428 int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1429 auto docbits = document["bits"];
1430 if (!docbits.is_null()) {
1431 if (!docbits.is_number() || (fmod(docbits.number_value(), 1.0) != 0) || docbits.int_value() < 0) {
1432 throw ApiException("'bits' must be a positive integer value");
1433 }
1434
1435 bits = docbits.int_value();
1436 }
1437 int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1438 const auto& providedAlgo = document["algorithm"];
1439 if (providedAlgo.is_string()) {
1440 algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
1441 if (algorithm == -1) {
1442 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
1443 }
1444 }
1445 else if (providedAlgo.is_number()) {
1446 algorithm = providedAlgo.int_value();
1447 }
1448 else if (!providedAlgo.is_null()) {
1449 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
1450 }
1451
1452 try {
1453 if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, keyOrZone, algorithm, insertedId, bits, active, published)) {
1454 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1455 }
1456 }
1457 catch (std::runtime_error& error) {
1458 throw ApiException(error.what());
1459 }
1460 if (insertedId < 0) {
1461 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1462 }
1463 }
1464 else if (document["bits"].is_null() && document["algorithm"].is_null()) {
1465 const auto& keyData = stringFromJson(document, privatekey_fieldname);
1466 DNSKEYRecordContent dkrc;
1467 DNSSECPrivateKey dpk;
1468 try {
1469 shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
1470 uint16_t flags = 0;
1471 if (keyOrZone) {
1472 flags = 257;
1473 }
1474 else {
1475 flags = 256;
1476 }
1477
1478 uint8_t algorithm = dkrc.d_algorithm;
1479 // TODO remove in 4.2.0
1480 if (algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1) {
1481 algorithm = DNSSECKeeper::RSASHA1;
1482 }
1483 dpk.setKey(dke, flags, algorithm);
1484 }
1485 catch (std::runtime_error& error) {
1486 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1487 }
1488 try {
1489 if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, dpk, insertedId, active, published)) {
1490 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1491 }
1492 }
1493 catch (std::runtime_error& error) {
1494 throw ApiException(error.what());
1495 }
1496 if (insertedId < 0) {
1497 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1498 }
1499 }
1500 else {
1501 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1502 }
1503 apiZoneCryptokeysExport(zoneData.zoneName, insertedId, resp, &zoneData.dnssecKeeper);
1504 resp->status = 201;
1505 }
1506
1507 /*
1508 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1509 * It de/activates a key from :zone_name specified by :cryptokey_id.
1510 * Server Answers:
1511 * Case 1: invalid JSON data
1512 * The server returns 400 Bad Request
1513 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1514 * The server returns 204 No Content
1515 * Case 3: the backend returns false on de/activation. An error occurred.
1516 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1517 * */
1518 static void apiZoneCryptokeysPUT(HttpRequest* req, HttpResponse* resp)
1519 {
1520 ZoneData zoneData{req};
1521 const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
1522
1523 if (inquireKeyId == -1) {
1524 throw HttpBadRequestException();
1525 }
1526 // throws an exception if the Body is empty
1527 const auto& document = req->json();
1528 // throws an exception if the key does not exist or is not a bool
1529 bool active = boolFromJson(document, "active");
1530 bool published = boolFromJson(document, "published", true);
1531 if (active) {
1532 if (!zoneData.dnssecKeeper.activateKey(zoneData.zoneName, inquireKeyId)) {
1533 resp->setErrorResult("Could not activate Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
1534 return;
1535 }
1536 }
1537 else {
1538 if (!zoneData.dnssecKeeper.deactivateKey(zoneData.zoneName, inquireKeyId)) {
1539 resp->setErrorResult("Could not deactivate Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
1540 return;
1541 }
1542 }
1543
1544 if (published) {
1545 if (!zoneData.dnssecKeeper.publishKey(zoneData.zoneName, inquireKeyId)) {
1546 resp->setErrorResult("Could not publish Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
1547 return;
1548 }
1549 }
1550 else {
1551 if (!zoneData.dnssecKeeper.unpublishKey(zoneData.zoneName, inquireKeyId)) {
1552 resp->setErrorResult("Could not unpublish Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
1553 return;
1554 }
1555 }
1556
1557 resp->body = "";
1558 resp->status = 204;
1559 }
1560
1561 static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, const DNSName& zonename)
1562 {
1563 DNSResourceRecord resourceRecord;
1564 vector<string> zonedata;
1565 stringtok(zonedata, zonestring, "\r\n");
1566
1567 ZoneParserTNG zpt(zonedata, zonename);
1568 zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
1569 zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
1570
1571 bool seenSOA = false;
1572
1573 string comment = "Imported via the API";
1574
1575 try {
1576 while (zpt.get(resourceRecord, &comment)) {
1577 if (seenSOA && resourceRecord.qtype.getCode() == QType::SOA) {
1578 continue;
1579 }
1580 if (resourceRecord.qtype.getCode() == QType::SOA) {
1581 seenSOA = true;
1582 }
1583 validateGatheredRRType(resourceRecord);
1584
1585 new_records.push_back(resourceRecord);
1586 }
1587 }
1588 catch (std::exception& ae) {
1589 throw ApiException("An error occurred while parsing the zonedata: " + string(ae.what()));
1590 }
1591 }
1592
1593 /** Throws ApiException if records which violate RRset constraints are present.
1594 * NOTE: sorts records in-place.
1595 *
1596 * Constraints being checked:
1597 * *) no exact duplicates
1598 * *) no duplicates for QTypes that can only be present once per RRset
1599 * *) hostnames are hostnames
1600 */
1601 static void checkNewRecords(vector<DNSResourceRecord>& records, const DNSName& zone)
1602 {
1603 sort(records.begin(), records.end(),
1604 [](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool {
1605 /* we need _strict_ weak ordering */
1606 return std::tie(rec_a.qname, rec_a.qtype, rec_a.content) < std::tie(rec_b.qname, rec_b.qtype, rec_b.content);
1607 });
1608
1609 DNSResourceRecord previous;
1610 for (const auto& rec : records) {
1611 if (previous.qname == rec.qname) {
1612 if (previous.qtype == rec.qtype) {
1613 if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
1614 throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " has more than one record");
1615 }
1616 if (previous.content == rec.content) {
1617 throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " with content \"" + rec.content + "\"");
1618 }
1619 }
1620 else if (exclusiveEntryTypes.count(rec.qtype.getCode()) != 0 || exclusiveEntryTypes.count(previous.qtype.getCode()) != 0) {
1621 throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + ": Conflicts with another RRset");
1622 }
1623 }
1624
1625 if (rec.qname == zone) {
1626 if (nonApexTypes.count(rec.qtype.getCode()) != 0) {
1627 throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is not allowed at apex");
1628 }
1629 }
1630 else if (atApexTypes.count(rec.qtype.getCode()) != 0) {
1631 throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is only allowed at apex");
1632 }
1633
1634 // Check if the DNSNames that should be hostnames, are hostnames
1635 try {
1636 checkHostnameCorrectness(rec);
1637 }
1638 catch (const std::exception& e) {
1639 throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + ": " + e.what());
1640 }
1641
1642 previous = rec;
1643 }
1644 }
1645
1646 static void checkTSIGKey(UeberBackend& backend, const DNSName& keyname, const DNSName& algo, const string& content)
1647 {
1648 DNSName algoFromDB;
1649 string contentFromDB;
1650 if (backend.getTSIGKey(keyname, algoFromDB, contentFromDB)) {
1651 throw HttpConflictException("A TSIG key with the name '" + keyname.toLogString() + "' already exists");
1652 }
1653
1654 TSIGHashEnum the{};
1655 if (!getTSIGHashEnum(algo, the)) {
1656 throw ApiException("Unknown TSIG algorithm: " + algo.toLogString());
1657 }
1658
1659 string b64out;
1660 if (B64Decode(content, b64out) == -1) {
1661 throw ApiException("TSIG content '" + content + "' cannot be base64-decoded");
1662 }
1663 }
1664
1665 static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content)
1666 {
1667 Json::object tsigkey = {
1668 {"name", keyname.toStringNoDot()},
1669 {"id", apiZoneNameToId(keyname)},
1670 {"algorithm", algo.toStringNoDot()},
1671 {"key", content},
1672 {"type", "TSIGKey"}};
1673 return tsigkey;
1674 }
1675
1676 static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent = true)
1677 {
1678 return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : "");
1679 }
1680
1681 static void apiServerTSIGKeysGET(HttpRequest* /* req */, HttpResponse* resp)
1682 {
1683 UeberBackend backend;
1684 vector<struct TSIGKey> keys;
1685
1686 if (!backend.getTSIGKeys(keys)) {
1687 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1688 }
1689
1690 Json::array doc;
1691
1692 for (const auto& key : keys) {
1693 doc.emplace_back(makeJSONTSIGKey(key, false));
1694 }
1695 resp->setJsonBody(doc);
1696 }
1697
1698 static void apiServerTSIGKeysPOST(HttpRequest* req, HttpResponse* resp)
1699 {
1700 UeberBackend backend;
1701 const auto& document = req->json();
1702 DNSName keyname(stringFromJson(document, "name"));
1703 DNSName algo(stringFromJson(document, "algorithm"));
1704 string content = document["key"].string_value();
1705
1706 if (content.empty()) {
1707 try {
1708 content = makeTSIGKey(algo);
1709 }
1710 catch (const PDNSException& exc) {
1711 throw HttpBadRequestException(exc.reason);
1712 }
1713 }
1714
1715 // Will throw an ApiException or HttpConflictException on error
1716 checkTSIGKey(backend, keyname, algo, content);
1717
1718 if (!backend.setTSIGKey(keyname, algo, content)) {
1719 throw HttpInternalServerErrorException("Unable to add TSIG key");
1720 }
1721
1722 resp->status = 201;
1723 resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content));
1724 }
1725
1726 class TSIGKeyData
1727 {
1728 public:
1729 TSIGKeyData(HttpRequest* req) :
1730 keyName(apiZoneIdToName(req->parameters["id"]))
1731 {
1732 try {
1733 if (!backend.getTSIGKey(keyName, algo, content)) {
1734 throw HttpNotFoundException("TSIG key with name '" + keyName.toLogString() + "' not found");
1735 }
1736 }
1737 catch (const PDNSException& e) {
1738 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason);
1739 }
1740
1741 tsigKey.name = keyName;
1742 tsigKey.algorithm = algo;
1743 tsigKey.key = std::move(content);
1744 }
1745
1746 UeberBackend backend;
1747 DNSName keyName;
1748 DNSName algo;
1749 string content;
1750 struct TSIGKey tsigKey;
1751 };
1752
1753 static void apiServerTSIGKeyDetailGET(HttpRequest* req, HttpResponse* resp)
1754 {
1755 TSIGKeyData tsigKeyData{req};
1756
1757 resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey));
1758 }
1759
1760 static void apiServerTSIGKeyDetailPUT(HttpRequest* req, HttpResponse* resp)
1761 {
1762 TSIGKeyData tsigKeyData{req};
1763
1764 const auto& document = req->json();
1765
1766 if (document["name"].is_string()) {
1767 tsigKeyData.tsigKey.name = DNSName(document["name"].string_value());
1768 }
1769 if (document["algorithm"].is_string()) {
1770 tsigKeyData.tsigKey.algorithm = DNSName(document["algorithm"].string_value());
1771
1772 TSIGHashEnum the{};
1773 if (!getTSIGHashEnum(tsigKeyData.tsigKey.algorithm, the)) {
1774 throw ApiException("Unknown TSIG algorithm: " + tsigKeyData.tsigKey.algorithm.toLogString());
1775 }
1776 }
1777 if (document["key"].is_string()) {
1778 string new_content = document["key"].string_value();
1779 string decoded;
1780 if (B64Decode(new_content, decoded) == -1) {
1781 throw ApiException("Can not base64 decode key content '" + new_content + "'");
1782 }
1783 tsigKeyData.tsigKey.key = std::move(new_content);
1784 }
1785 if (!tsigKeyData.backend.setTSIGKey(tsigKeyData.tsigKey.name, tsigKeyData.tsigKey.algorithm, tsigKeyData.tsigKey.key)) {
1786 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1787 }
1788 if (tsigKeyData.tsigKey.name != tsigKeyData.keyName) {
1789 // Remove the old key
1790 if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) {
1791 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'");
1792 }
1793 }
1794 resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey));
1795 }
1796
1797 static void apiServerTSIGKeyDetailDELETE(HttpRequest* req, HttpResponse* resp)
1798 {
1799 TSIGKeyData tsigKeyData{req};
1800 if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) {
1801 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'");
1802 }
1803 resp->body = "";
1804 resp->status = 204;
1805 }
1806
1807 static void apiServerAutoprimaryDetailDELETE(HttpRequest* req, HttpResponse* resp)
1808 {
1809 UeberBackend backend;
1810 const AutoPrimary& primary{req->parameters["ip"], req->parameters["nameserver"], ""};
1811 if (!backend.autoPrimaryRemove(primary)) {
1812 throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
1813 }
1814 resp->body = "";
1815 resp->status = 204;
1816 }
1817
1818 static void apiServerAutoprimariesGET(HttpRequest* /* req */, HttpResponse* resp)
1819 {
1820 UeberBackend backend;
1821
1822 std::vector<AutoPrimary> primaries;
1823 if (!backend.autoPrimariesList(primaries)) {
1824 throw HttpInternalServerErrorException("Unable to retrieve autoprimaries");
1825 }
1826 Json::array doc;
1827 for (const auto& primary : primaries) {
1828 const Json::object obj = {
1829 {"ip", primary.ip},
1830 {"nameserver", primary.nameserver},
1831 {"account", primary.account}};
1832 doc.emplace_back(obj);
1833 }
1834 resp->setJsonBody(doc);
1835 }
1836
1837 static void apiServerAutoprimariesPOST(HttpRequest* req, HttpResponse* resp)
1838 {
1839 UeberBackend backend;
1840
1841 const auto& document = req->json();
1842
1843 AutoPrimary primary(stringFromJson(document, "ip"), stringFromJson(document, "nameserver"), "");
1844
1845 if (document["account"].is_string()) {
1846 primary.account = document["account"].string_value();
1847 }
1848
1849 if (primary.ip.empty() or primary.nameserver.empty()) {
1850 throw ApiException("ip and nameserver fields must be filled");
1851 }
1852 if (!backend.autoPrimaryAdd(primary)) {
1853 throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
1854 }
1855 resp->body = "";
1856 resp->status = 201;
1857 }
1858
1859 // create new zone
1860 static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp)
1861 {
1862 UeberBackend backend;
1863 DNSSECKeeper dnssecKeeper(&backend);
1864 DomainInfo domainInfo;
1865 const auto& document = req->json();
1866 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1867 apiCheckNameAllowedCharacters(zonename.toString());
1868 zonename.makeUsLowerCase();
1869
1870 bool exists = backend.getDomainInfo(zonename, domainInfo);
1871 if (exists) {
1872 throw HttpConflictException();
1873 }
1874
1875 boost::optional<DomainInfo::DomainKind> kind;
1876 boost::optional<vector<ComboAddress>> primaries;
1877 boost::optional<DNSName> catalog;
1878 boost::optional<string> account;
1879 extractDomainInfoFromDocument(document, kind, primaries, catalog, account);
1880
1881 // validate 'kind' is set
1882 if (!kind) {
1883 throw JsonException("Key 'kind' not present or not a String");
1884 }
1885 DomainInfo::DomainKind zonekind = *kind;
1886
1887 string zonestring = document["zone"].string_value();
1888 auto rrsets = document["rrsets"];
1889 if (rrsets.is_array() && !zonestring.empty()) {
1890 throw ApiException("You cannot give rrsets AND zone data as text");
1891 }
1892
1893 const auto& nameservers = document["nameservers"];
1894 if (!nameservers.is_null() && !nameservers.is_array() && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) {
1895 throw ApiException("Nameservers is not a list");
1896 }
1897
1898 // if records/comments are given, load and check them
1899 bool have_soa = false;
1900 bool have_zone_ns = false;
1901 vector<DNSResourceRecord> new_records;
1902 vector<Comment> new_comments;
1903
1904 try {
1905 if (rrsets.is_array()) {
1906 for (const auto& rrset : rrsets.array_items()) {
1907 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1908 apiCheckQNameAllowedCharacters(qname.toString());
1909 QType qtype;
1910 qtype = stringFromJson(rrset, "type");
1911 if (qtype.getCode() == 0) {
1912 throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
1913 }
1914 if (rrset["records"].is_array()) {
1915 uint32_t ttl = uintFromJson(rrset, "ttl");
1916 gatherRecords(rrset, qname, qtype, ttl, new_records);
1917 }
1918 if (rrset["comments"].is_array()) {
1919 gatherComments(rrset, qname, qtype, new_comments);
1920 }
1921 }
1922 }
1923 else if (!zonestring.empty()) {
1924 gatherRecordsFromZone(zonestring, new_records, zonename);
1925 }
1926 }
1927 catch (const JsonException& exc) {
1928 throw ApiException("New RRsets are invalid: " + string(exc.what()));
1929 }
1930
1931 if (zonekind == DomainInfo::Consumer && !new_records.empty()) {
1932 throw ApiException("Zone data MUST NOT be given for Consumer zones");
1933 }
1934
1935 for (auto& resourceRecord : new_records) {
1936 resourceRecord.qname.makeUsLowerCase();
1937 if (!resourceRecord.qname.isPartOf(zonename) && resourceRecord.qname != zonename) {
1938 throw ApiException("RRset " + resourceRecord.qname.toString() + " IN " + resourceRecord.qtype.toString() + ": Name is out of zone");
1939 }
1940
1941 apiCheckQNameAllowedCharacters(resourceRecord.qname.toString());
1942
1943 if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) {
1944 have_soa = true;
1945 }
1946 if (resourceRecord.qtype.getCode() == QType::NS && resourceRecord.qname == zonename) {
1947 have_zone_ns = true;
1948 }
1949 }
1950
1951 // synthesize RRs as needed
1952 DNSResourceRecord autorr;
1953 autorr.qname = zonename;
1954 autorr.auth = true;
1955 autorr.ttl = ::arg().asNum("default-ttl");
1956
1957 if (!have_soa && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) {
1958 // synthesize a SOA record so the zone "really" exists
1959 string soa = ::arg()["default-soa-content"];
1960 boost::replace_all(soa, "@", zonename.toStringNoDot());
1961 SOAData soaData;
1962 fillSOAData(soa, soaData);
1963 soaData.serial = document["serial"].int_value();
1964 autorr.qtype = QType::SOA;
1965 autorr.content = makeSOAContent(soaData)->getZoneRepresentation(true);
1966 // updateDomainSettingsFromDocument will apply SOA-EDIT-API as needed
1967 new_records.push_back(autorr);
1968 }
1969
1970 // create NS records if nameservers are given
1971 for (const auto& value : nameservers.array_items()) {
1972 const string& nameserver = value.string_value();
1973 if (nameserver.empty()) {
1974 throw ApiException("Nameservers must be non-empty strings");
1975 }
1976 if (zonekind == DomainInfo::Consumer) {
1977 throw ApiException("Nameservers MUST NOT be given for Consumer zones");
1978 }
1979 if (!isCanonical(nameserver)) {
1980 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
1981 }
1982 try {
1983 // ensure the name parses
1984 autorr.content = DNSName(nameserver).toStringRootDot();
1985 }
1986 catch (...) {
1987 throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
1988 }
1989 autorr.qtype = QType::NS;
1990 new_records.push_back(autorr);
1991 if (have_zone_ns) {
1992 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1993 }
1994 }
1995
1996 checkNewRecords(new_records, zonename);
1997
1998 if (boolFromJson(document, "dnssec", false)) {
1999 checkDefaultDNSSECAlgos();
2000
2001 if (document["nsec3param"].string_value().length() > 0) {
2002 NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
2003 string error_msg;
2004 if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) {
2005 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg);
2006 }
2007 }
2008 }
2009
2010 // no going back after this
2011 if (!backend.createDomain(zonename, kind.get_value_or(DomainInfo::Native), primaries.get_value_or(vector<ComboAddress>()), account.get_value_or(""))) {
2012 throw ApiException("Creating domain '" + zonename.toString() + "' failed: backend refused");
2013 }
2014
2015 if (!backend.getDomainInfo(zonename, domainInfo)) {
2016 throw ApiException("Creating domain '" + zonename.toString() + "' failed: lookup of domain ID failed");
2017 }
2018
2019 domainInfo.backend->startTransaction(zonename, static_cast<int>(domainInfo.id));
2020
2021 // will be overridden by updateDomainSettingsFromDocument, if given in document.
2022 domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", "DEFAULT");
2023
2024 for (auto& resourceRecord : new_records) {
2025 resourceRecord.domain_id = static_cast<int>(domainInfo.id);
2026 domainInfo.backend->feedRecord(resourceRecord, DNSName());
2027 }
2028 for (Comment& comment : new_comments) {
2029 comment.domain_id = static_cast<int>(domainInfo.id);
2030 if (!domainInfo.backend->feedComment(comment)) {
2031 throw ApiException("Hosting backend does not support editing comments.");
2032 }
2033 }
2034
2035 updateDomainSettingsFromDocument(backend, domainInfo, zonename, document, !new_records.empty());
2036
2037 if (!catalog && kind == DomainInfo::Primary) {
2038 const auto& defaultCatalog = ::arg()["default-catalog-zone"];
2039 if (!defaultCatalog.empty()) {
2040 domainInfo.backend->setCatalog(zonename, DNSName(defaultCatalog));
2041 }
2042 }
2043
2044 domainInfo.backend->commitTransaction();
2045
2046 g_zoneCache.add(zonename, static_cast<int>(domainInfo.id)); // make new zone visible
2047
2048 fillZone(backend, zonename, resp, req);
2049 resp->status = 201;
2050 }
2051
2052 // list known zones
2053 static void apiServerZonesGET(HttpRequest* req, HttpResponse* resp)
2054 {
2055 UeberBackend backend;
2056 DNSSECKeeper dnssecKeeper(&backend);
2057 vector<DomainInfo> domains;
2058
2059 if (req->getvars.count("zone") != 0) {
2060 string zone = req->getvars["zone"];
2061 apiCheckNameAllowedCharacters(zone);
2062 DNSName zonename = apiNameToDNSName(zone);
2063 zonename.makeUsLowerCase();
2064 DomainInfo domainInfo;
2065 if (backend.getDomainInfo(zonename, domainInfo)) {
2066 domains.push_back(domainInfo);
2067 }
2068 }
2069 else {
2070 try {
2071 backend.getAllDomains(&domains, true, true); // incl. serial and disabled
2072 }
2073 catch (const PDNSException& exception) {
2074 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + exception.reason);
2075 }
2076 }
2077
2078 bool with_dnssec = true;
2079 if (req->getvars.count("dnssec") != 0) {
2080 // can send ?dnssec=false to improve performance.
2081 string dnssec_flag = req->getvars["dnssec"];
2082 if (dnssec_flag == "false") {
2083 with_dnssec = false;
2084 }
2085 }
2086
2087 Json::array doc;
2088 doc.reserve(domains.size());
2089 for (const DomainInfo& domainInfo : domains) {
2090 doc.emplace_back(getZoneInfo(domainInfo, with_dnssec ? &dnssecKeeper : nullptr));
2091 }
2092 resp->setJsonBody(doc);
2093 }
2094
2095 static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp)
2096 {
2097 ZoneData zoneData{req};
2098
2099 // update domain contents and/or settings
2100 const auto& document = req->json();
2101
2102 auto rrsets = document["rrsets"];
2103 bool zoneWasModified = false;
2104 DomainInfo::DomainKind newKind = zoneData.domainInfo.kind;
2105 if (document["kind"].is_string()) {
2106 newKind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
2107 }
2108
2109 // if records/comments are given, load, check and insert them
2110 if (rrsets.is_array()) {
2111 zoneWasModified = true;
2112 bool haveSoa = false;
2113 string soaEditApiKind;
2114 string soaEditKind;
2115 zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT-API", soaEditApiKind);
2116 zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT", soaEditKind);
2117
2118 vector<DNSResourceRecord> new_records;
2119 vector<Comment> new_comments;
2120
2121 try {
2122 for (const auto& rrset : rrsets.array_items()) {
2123 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
2124 apiCheckQNameAllowedCharacters(qname.toString());
2125 QType qtype;
2126 qtype = stringFromJson(rrset, "type");
2127 if (qtype.getCode() == 0) {
2128 throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
2129 }
2130 if (rrset["records"].is_array()) {
2131 uint32_t ttl = uintFromJson(rrset, "ttl");
2132 gatherRecords(rrset, qname, qtype, ttl, new_records);
2133 }
2134 if (rrset["comments"].is_array()) {
2135 gatherComments(rrset, qname, qtype, new_comments);
2136 }
2137 }
2138 }
2139 catch (const JsonException& exc) {
2140 throw ApiException("New RRsets are invalid: " + string(exc.what()));
2141 }
2142
2143 for (auto& resourceRecord : new_records) {
2144 resourceRecord.qname.makeUsLowerCase();
2145 if (!resourceRecord.qname.isPartOf(zoneData.zoneName) && resourceRecord.qname != zoneData.zoneName) {
2146 throw ApiException("RRset " + resourceRecord.qname.toString() + " IN " + resourceRecord.qtype.toString() + ": Name is out of zone");
2147 }
2148 apiCheckQNameAllowedCharacters(resourceRecord.qname.toString());
2149
2150 if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zoneData.zoneName) {
2151 haveSoa = true;
2152 }
2153 }
2154
2155 if (!haveSoa && newKind != DomainInfo::Secondary && newKind != DomainInfo::Consumer) {
2156 // Require SOA if this is a primary zone.
2157 throw ApiException("Must give SOA record for zone when replacing all RR sets");
2158 }
2159 if (newKind == DomainInfo::Consumer && !new_records.empty()) {
2160 // Allow deleting all RRsets, just not modifying them.
2161 throw ApiException("Modifying RRsets in Consumer zones is unsupported");
2162 }
2163
2164 checkNewRecords(new_records, zoneData.zoneName);
2165
2166 zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id));
2167 for (auto& resourceRecord : new_records) {
2168 resourceRecord.domain_id = static_cast<int>(zoneData.domainInfo.id);
2169 zoneData.domainInfo.backend->feedRecord(resourceRecord, DNSName());
2170 }
2171 for (Comment& comment : new_comments) {
2172 comment.domain_id = static_cast<int>(zoneData.domainInfo.id);
2173 zoneData.domainInfo.backend->feedComment(comment);
2174 }
2175
2176 if (!haveSoa && (newKind == DomainInfo::Secondary || newKind == DomainInfo::Consumer)) {
2177 zoneData.domainInfo.backend->setStale(zoneData.domainInfo.id);
2178 }
2179 }
2180 else {
2181 // avoid deleting current zone contents
2182 zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1);
2183 }
2184
2185 // updateDomainSettingsFromDocument will rectify the zone and update SOA serial.
2186 updateDomainSettingsFromDocument(zoneData.backend, zoneData.domainInfo, zoneData.zoneName, document, zoneWasModified);
2187 zoneData.domainInfo.backend->commitTransaction();
2188
2189 purgeAuthCaches(zoneData.zoneName.toString() + "$");
2190
2191 resp->body = "";
2192 resp->status = 204; // No Content, but indicate success
2193 }
2194
2195 static void apiServerZoneDetailDELETE(HttpRequest* req, HttpResponse* resp)
2196 {
2197 ZoneData zoneData{req};
2198
2199 // delete domain
2200
2201 zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1);
2202 try {
2203 if (!zoneData.domainInfo.backend->deleteDomain(zoneData.zoneName)) {
2204 throw ApiException("Deleting domain '" + zoneData.zoneName.toString() + "' failed: backend delete failed/unsupported");
2205 }
2206
2207 zoneData.domainInfo.backend->commitTransaction();
2208
2209 g_zoneCache.remove(zoneData.zoneName);
2210 }
2211 catch (...) {
2212 zoneData.domainInfo.backend->abortTransaction();
2213 throw;
2214 }
2215
2216 // clear caches
2217 DNSSECKeeper::clearCaches(zoneData.zoneName);
2218 purgeAuthCaches(zoneData.zoneName.toString() + "$");
2219
2220 // empty body on success
2221 resp->body = "";
2222 resp->status = 204; // No Content: declare that the zone is gone now
2223 }
2224
2225 static void apiServerZoneDetailPATCH(HttpRequest* req, HttpResponse* resp)
2226 {
2227 ZoneData zoneData{req};
2228 patchZone(zoneData.backend, zoneData.zoneName, zoneData.domainInfo, req, resp);
2229 }
2230
2231 static void apiServerZoneDetailGET(HttpRequest* req, HttpResponse* resp)
2232 {
2233 ZoneData zoneData{req};
2234 fillZone(zoneData.backend, zoneData.zoneName, resp, req);
2235 }
2236
2237 static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp)
2238 {
2239 ZoneData zoneData{req};
2240
2241 ostringstream outputStringStream;
2242
2243 DNSResourceRecord resourceRecord;
2244 SOAData soaData;
2245 zoneData.domainInfo.backend->list(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id));
2246 while (zoneData.domainInfo.backend->get(resourceRecord)) {
2247 if (resourceRecord.qtype.getCode() == 0) {
2248 continue; // skip empty non-terminals
2249 }
2250
2251 outputStringStream << resourceRecord.qname.toString() << "\t" << resourceRecord.ttl << "\t"
2252 << "IN"
2253 << "\t" << resourceRecord.qtype.toString() << "\t" << makeApiRecordContent(resourceRecord.qtype, resourceRecord.content) << endl;
2254 }
2255
2256 if (req->accept_json) {
2257 resp->setJsonBody(Json::object{{"zone", outputStringStream.str()}});
2258 }
2259 else {
2260 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
2261 resp->body = outputStringStream.str();
2262 }
2263 }
2264
2265 static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp)
2266 {
2267 ZoneData zoneData{req};
2268
2269 if (zoneData.domainInfo.primaries.empty()) {
2270 throw ApiException("Domain '" + zoneData.zoneName.toString() + "' is not a secondary domain (or has no primary defined)");
2271 }
2272
2273 shuffle(zoneData.domainInfo.primaries.begin(), zoneData.domainInfo.primaries.end(), pdns::dns_random_engine());
2274 Communicator.addSuckRequest(zoneData.zoneName, zoneData.domainInfo.primaries.front(), SuckRequest::Api);
2275 resp->setSuccessResult("Added retrieval request for '" + zoneData.zoneName.toString() + "' from primary " + zoneData.domainInfo.primaries.front().toLogString());
2276 }
2277
2278 static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp)
2279 {
2280 ZoneData zoneData{req};
2281
2282 if (!Communicator.notifyDomain(zoneData.zoneName, &zoneData.backend)) {
2283 throw ApiException("Failed to add to the queue - see server log");
2284 }
2285
2286 resp->setSuccessResult("Notification queued");
2287 }
2288
2289 static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp)
2290 {
2291 ZoneData zoneData{req};
2292
2293 if (zoneData.dnssecKeeper.isPresigned(zoneData.zoneName)) {
2294 throw ApiException("Zone '" + zoneData.zoneName.toString() + "' is pre-signed, not rectifying.");
2295 }
2296
2297 string error_msg;
2298 string info;
2299 if (!zoneData.dnssecKeeper.rectifyZone(zoneData.zoneName, error_msg, info, true)) {
2300 throw ApiException("Failed to rectify '" + zoneData.zoneName.toString() + "' " + error_msg);
2301 }
2302
2303 resp->setSuccessResult("Rectified");
2304 }
2305
2306 // NOLINTNEXTLINE(readability-function-cognitive-complexity): TODO Refactor this function.
2307 static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp)
2308 {
2309 bool zone_disabled = false;
2310 SOAData soaData;
2311
2312 vector<DNSResourceRecord> new_records;
2313 vector<Comment> new_comments;
2314 vector<DNSResourceRecord> new_ptrs;
2315
2316 Json document = req->json();
2317
2318 auto rrsets = document["rrsets"];
2319 if (!rrsets.is_array()) {
2320 throw ApiException("No rrsets given in update request");
2321 }
2322
2323 domainInfo.backend->startTransaction(zonename);
2324
2325 try {
2326 string soa_edit_api_kind;
2327 string soa_edit_kind;
2328 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
2329 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
2330 bool soa_edit_done = false;
2331
2332 set<std::tuple<DNSName, QType, string>> seen;
2333
2334 for (const auto& rrset : rrsets.array_items()) {
2335 string changetype = toUpper(stringFromJson(rrset, "changetype"));
2336 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
2337 apiCheckQNameAllowedCharacters(qname.toString());
2338 QType qtype;
2339 qtype = stringFromJson(rrset, "type");
2340 if (qtype.getCode() == 0) {
2341 throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
2342 }
2343
2344 if (seen.count({qname, qtype, changetype}) != 0) {
2345 throw ApiException("Duplicate RRset " + qname.toString() + " IN " + qtype.toString() + " with changetype: " + changetype);
2346 }
2347 seen.insert({qname, qtype, changetype});
2348
2349 if (changetype == "DELETE") {
2350 // delete all matching qname/qtype RRs (and, implicitly comments).
2351 if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, vector<DNSResourceRecord>())) {
2352 throw ApiException("Hosting backend does not support editing records.");
2353 }
2354 }
2355 else if (changetype == "REPLACE") {
2356 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
2357 if (!qname.isPartOf(zonename) && qname != zonename) {
2358 throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Name is out of zone");
2359 }
2360
2361 bool replace_records = rrset["records"].is_array();
2362 bool replace_comments = rrset["comments"].is_array();
2363
2364 if (!replace_records && !replace_comments) {
2365 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.toString());
2366 }
2367
2368 new_records.clear();
2369 new_comments.clear();
2370
2371 try {
2372 if (replace_records) {
2373 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2374 uint32_t ttl = uintFromJson(rrset, "ttl");
2375 gatherRecords(rrset, qname, qtype, ttl, new_records);
2376
2377 for (DNSResourceRecord& resourceRecord : new_records) {
2378 resourceRecord.domain_id = static_cast<int>(domainInfo.id);
2379 if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) {
2380 soa_edit_done = increaseSOARecord(resourceRecord, soa_edit_api_kind, soa_edit_kind);
2381 }
2382 }
2383 checkNewRecords(new_records, zonename);
2384 }
2385
2386 if (replace_comments) {
2387 gatherComments(rrset, qname, qtype, new_comments);
2388
2389 for (Comment& comment : new_comments) {
2390 comment.domain_id = static_cast<int>(domainInfo.id);
2391 }
2392 }
2393 }
2394 catch (const JsonException& e) {
2395 throw ApiException("New RRsets are invalid: " + string(e.what()));
2396 }
2397
2398 if (replace_records) {
2399 bool ent_present = false;
2400 bool dname_seen = false;
2401 bool ns_seen = false;
2402
2403 domainInfo.backend->lookup(QType(QType::ANY), qname, static_cast<int>(domainInfo.id));
2404 DNSResourceRecord resourceRecord;
2405 while (domainInfo.backend->get(resourceRecord)) {
2406 if (resourceRecord.qtype.getCode() == QType::ENT) {
2407 ent_present = true;
2408 /* that's fine, we will override it */
2409 continue;
2410 }
2411 if (qtype == QType::DNAME || resourceRecord.qtype == QType::DNAME) {
2412 dname_seen = true;
2413 }
2414 if (qtype == QType::NS || resourceRecord.qtype == QType::NS) {
2415 ns_seen = true;
2416 }
2417 if (qtype.getCode() != resourceRecord.qtype.getCode()
2418 && (exclusiveEntryTypes.count(qtype.getCode()) != 0
2419 || exclusiveEntryTypes.count(resourceRecord.qtype.getCode()) != 0)) {
2420
2421 // leave database handle in a consistent state
2422 while (domainInfo.backend->get(resourceRecord)) {
2423 ;
2424 }
2425
2426 throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Conflicts with pre-existing RRset");
2427 }
2428 }
2429
2430 if (dname_seen && ns_seen && qname != zonename) {
2431 throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Cannot have both NS and DNAME except in zone apex");
2432 }
2433 if (!new_records.empty() && domainInfo.kind == DomainInfo::Consumer) {
2434 // Allow deleting all RRsets, just not modifying them.
2435 throw ApiException("Modifying RRsets in Consumer zones is unsupported");
2436 }
2437 if (!new_records.empty() && ent_present) {
2438 QType qt_ent{0};
2439 if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qt_ent, new_records)) {
2440 throw ApiException("Hosting backend does not support editing records.");
2441 }
2442 }
2443 if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, new_records)) {
2444 throw ApiException("Hosting backend does not support editing records.");
2445 }
2446 }
2447 if (replace_comments) {
2448 if (!domainInfo.backend->replaceComments(domainInfo.id, qname, qtype, new_comments)) {
2449 throw ApiException("Hosting backend does not support editing comments.");
2450 }
2451 }
2452 }
2453 else {
2454 throw ApiException("Changetype not understood");
2455 }
2456 }
2457
2458 zone_disabled = (!backend.getSOAUncached(zonename, soaData));
2459
2460 // edit SOA (if needed)
2461 if (!zone_disabled && !soa_edit_api_kind.empty() && !soa_edit_done) {
2462 DNSResourceRecord resourceRecord;
2463 if (makeIncreasedSOARecord(soaData, soa_edit_api_kind, soa_edit_kind, resourceRecord)) {
2464 if (!domainInfo.backend->replaceRRSet(domainInfo.id, resourceRecord.qname, resourceRecord.qtype, vector<DNSResourceRecord>(1, resourceRecord))) {
2465 throw ApiException("Hosting backend does not support editing records.");
2466 }
2467 }
2468
2469 // return old and new serials in headers
2470 resp->headers["X-PDNS-Old-Serial"] = std::to_string(soaData.serial);
2471 fillSOAData(resourceRecord.content, soaData);
2472 resp->headers["X-PDNS-New-Serial"] = std::to_string(soaData.serial);
2473 }
2474 }
2475 catch (...) {
2476 domainInfo.backend->abortTransaction();
2477 throw;
2478 }
2479
2480 // Rectify
2481 DNSSECKeeper dnssecKeeper(&backend);
2482 if (!zone_disabled && !dnssecKeeper.isPresigned(zonename) && isZoneApiRectifyEnabled(domainInfo)) {
2483 string info;
2484 string error_msg;
2485 if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false)) {
2486 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
2487 }
2488 }
2489
2490 domainInfo.backend->commitTransaction();
2491
2492 DNSSECKeeper::clearCaches(zonename);
2493 purgeAuthCaches(zonename.toString() + "$");
2494
2495 resp->body = "";
2496 resp->status = 204; // No Content, but indicate success
2497 }
2498
2499 static void apiServerSearchData(HttpRequest* req, HttpResponse* resp)
2500 {
2501 string qVar = req->getvars["q"];
2502 string sMaxVar = req->getvars["max"];
2503 string sObjectTypeVar = req->getvars["object_type"];
2504
2505 size_t maxEnts = 100;
2506 size_t ents = 0;
2507
2508 // the following types of data can be searched for using the api
2509 enum class ObjectType
2510 {
2511 ALL,
2512 ZONE,
2513 RECORD,
2514 COMMENT
2515 } objectType{};
2516
2517 if (qVar.empty()) {
2518 throw ApiException("Query q can't be blank");
2519 }
2520 if (!sMaxVar.empty()) {
2521 maxEnts = std::stoi(sMaxVar);
2522 }
2523 if (maxEnts < 1) {
2524 throw ApiException("Maximum entries must be larger than 0");
2525 }
2526
2527 if (sObjectTypeVar.empty() || sObjectTypeVar == "all") {
2528 objectType = ObjectType::ALL;
2529 }
2530 else if (sObjectTypeVar == "zone") {
2531 objectType = ObjectType::ZONE;
2532 }
2533 else if (sObjectTypeVar == "record") {
2534 objectType = ObjectType::RECORD;
2535 }
2536 else if (sObjectTypeVar == "comment") {
2537 objectType = ObjectType::COMMENT;
2538 }
2539 else {
2540 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2541 }
2542
2543 SimpleMatch simpleMatch(qVar, true);
2544 UeberBackend backend;
2545 vector<DomainInfo> domains;
2546 vector<DNSResourceRecord> result_rr;
2547 vector<Comment> result_c;
2548 map<int, DomainInfo> zoneIdZone;
2549 map<int, DomainInfo>::iterator val;
2550 Json::array doc;
2551
2552 backend.getAllDomains(&domains, false, true);
2553
2554 for (const DomainInfo& domainInfo : domains) {
2555 if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && simpleMatch.match(domainInfo.zone)) {
2556 doc.push_back(Json::object{
2557 {"object_type", "zone"},
2558 {"zone_id", apiZoneNameToId(domainInfo.zone)},
2559 {"name", domainInfo.zone.toString()}});
2560 ents++;
2561 }
2562 zoneIdZone[static_cast<int>(domainInfo.id)] = domainInfo; // populate cache
2563 }
2564
2565 if ((objectType == ObjectType::ALL || objectType == ObjectType::RECORD) && backend.searchRecords(qVar, maxEnts, result_rr)) {
2566 for (const DNSResourceRecord& resourceRecord : result_rr) {
2567 if (resourceRecord.qtype.getCode() == 0) {
2568 continue; // skip empty non-terminals
2569 }
2570
2571 auto object = Json::object{
2572 {"object_type", "record"},
2573 {"name", resourceRecord.qname.toString()},
2574 {"type", resourceRecord.qtype.toString()},
2575 {"ttl", (double)resourceRecord.ttl},
2576 {"disabled", resourceRecord.disabled},
2577 {"content", makeApiRecordContent(resourceRecord.qtype, resourceRecord.content)}};
2578
2579 val = zoneIdZone.find(resourceRecord.domain_id);
2580 if (val != zoneIdZone.end()) {
2581 object["zone_id"] = apiZoneNameToId(val->second.zone);
2582 object["zone"] = val->second.zone.toString();
2583 }
2584 doc.emplace_back(object);
2585 }
2586 }
2587
2588 if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && backend.searchComments(qVar, maxEnts, result_c)) {
2589 for (const Comment& comment : result_c) {
2590 auto object = Json::object{
2591 {"object_type", "comment"},
2592 {"name", comment.qname.toString()},
2593 {"type", comment.qtype.toString()},
2594 {"content", comment.content}};
2595
2596 val = zoneIdZone.find(comment.domain_id);
2597 if (val != zoneIdZone.end()) {
2598 object["zone_id"] = apiZoneNameToId(val->second.zone);
2599 object["zone"] = val->second.zone.toString();
2600 }
2601 doc.emplace_back(object);
2602 }
2603 }
2604
2605 resp->setJsonBody(doc);
2606 }
2607
2608 static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp)
2609 {
2610 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
2611
2612 if (g_zoneCache.isEnabled()) {
2613 DomainInfo domainInfo;
2614 UeberBackend backend;
2615 if (backend.getDomainInfo(canon, domainInfo, false)) {
2616 // zone exists (uncached), add/update it in the zone cache.
2617 // Handle this first, to avoid concurrent queries re-populating the other caches.
2618 g_zoneCache.add(domainInfo.zone, static_cast<int>(domainInfo.id));
2619 }
2620 else {
2621 g_zoneCache.remove(domainInfo.zone);
2622 }
2623 }
2624
2625 DNSSECKeeper::clearCaches(canon);
2626 // purge entire zone from cache, not just zone-level records.
2627 uint64_t count = purgeAuthCaches(canon.toString() + "$");
2628 resp->setJsonBody(Json::object{
2629 {"count", (int)count},
2630 {"result", "Flushed cache."}});
2631 }
2632
2633 static std::ostream& operator<<(std::ostream& outStream, StatType statType)
2634 {
2635 switch (statType) {
2636 case StatType::counter:
2637 return outStream << "counter";
2638 case StatType::gauge:
2639 return outStream << "gauge";
2640 };
2641 return outStream << static_cast<uint16_t>(statType);
2642 }
2643
2644 static void prometheusMetrics(HttpRequest* /* req */, HttpResponse* resp)
2645 {
2646 std::ostringstream output;
2647 for (const auto& metricName : S.getEntries()) {
2648 // Prometheus suggest using '_' instead of '-'
2649 std::string prometheusMetricName = "pdns_auth_" + boost::replace_all_copy(metricName, "-", "_");
2650
2651 output << "# HELP " << prometheusMetricName << " " << S.getDescrip(metricName) << "\n";
2652 output << "# TYPE " << prometheusMetricName << " " << S.getStatType(metricName) << "\n";
2653 output << prometheusMetricName << " " << S.read(metricName) << "\n";
2654 }
2655
2656 output << "# HELP pdns_auth_info "
2657 << "Info from PowerDNS, value is always 1"
2658 << "\n";
2659 output << "# TYPE pdns_auth_info "
2660 << "gauge"
2661 << "\n";
2662 output << "pdns_auth_info{version=\"" << VERSION << "\"} "
2663 << "1"
2664 << "\n";
2665
2666 resp->body = output.str();
2667 resp->headers["Content-Type"] = "text/plain";
2668 resp->status = 200;
2669 }
2670
2671 static void cssfunction(HttpRequest* /* req */, HttpResponse* resp)
2672 {
2673 resp->headers["Cache-Control"] = "max-age=86400";
2674 resp->headers["Content-Type"] = "text/css";
2675
2676 ostringstream ret;
2677 ret << "* { box-sizing: border-box; margin: 0; padding: 0; }" << endl;
2678 ret << "body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }" << endl;
2679 ret << "a { color: #0959c2; }" << endl;
2680 ret << "a:hover { color: #3B8EC8; }" << endl;
2681 ret << ".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }" << endl;
2682 ret << ".row:before, .row:after { display: table; content:\" \"; }" << endl;
2683 ret << ".row:after { clear: both; }" << endl;
2684 ret << ".columns { position: relative; min-height: 1px; float: left; }" << endl;
2685 ret << ".all { width: 100%; }" << endl;
2686 ret << ".headl { width: 60%; }" << endl;
2687 ret << ".header { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2688 ret << "background-image: url(data:image/png;base64,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);";
2689 ret << " width: 154px; height: 20px; }" << endl;
2690 ret << "a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }" << endl;
2691 ret << "footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }" << endl;
2692 ret << "footer.row { margin-top: 1em; margin-bottom: 1em; }" << endl;
2693 ret << ".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }" << endl;
2694 ret << "table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }" << endl;
2695 ret << "table.data td { border-bottom: 1px solid #333; padding: 2px; }" << endl;
2696 ret << "table.data tr:nth-child(2n) { background: #e2e2e2; }" << endl;
2697 ret << "table.data tr:hover { background: white; }" << endl;
2698 ret << ".ringmeta { margin-bottom: 5px; }" << endl;
2699 ret << ".resetring {float: right; }" << endl;
2700 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;
2701 ret << ".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}" << endl;
2702 ret << ".resizering {float: right;}" << endl;
2703 resp->body = ret.str();
2704 resp->status = 200;
2705 }
2706
2707 void AuthWebServer::webThread()
2708 {
2709 try {
2710 setThreadName("pdns/webserver");
2711 if (::arg().mustDo("api")) {
2712 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", apiServerCacheFlush, "PUT");
2713 d_ws->registerApiHandler("/api/v1/servers/localhost/config", apiServerConfig, "GET");
2714 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", apiServerSearchData, "GET");
2715 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", apiServerStatistics, "GET");
2716 d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries/<ip>/<nameserver>", &apiServerAutoprimaryDetailDELETE, "DELETE");
2717 d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesGET, "GET");
2718 d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesPOST, "POST");
2719 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailGET, "GET");
2720 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailPUT, "PUT");
2721 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailDELETE, "DELETE");
2722 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysGET, "GET");
2723 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysPOST, "POST");
2724 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", apiServerZoneAxfrRetrieve, "PUT");
2725 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysGET, "GET");
2726 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysPOST, "POST");
2727 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysPUT, "PUT");
2728 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysDELETE, "DELETE");
2729 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeysGET, "GET");
2730 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeysPOST, "POST");
2731 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", apiServerZoneExport, "GET");
2732 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindGET, "GET");
2733 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindPUT, "PUT");
2734 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindDELETE, "DELETE");
2735 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadataGET, "GET");
2736 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadataPOST, "POST");
2737 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", apiServerZoneNotify, "PUT");
2738 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", apiServerZoneRectify, "PUT");
2739 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailGET, "GET");
2740 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPATCH, "PATCH");
2741 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPUT, "PUT");
2742 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailDELETE, "DELETE");
2743 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesGET, "GET");
2744 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesPOST, "POST");
2745 d_ws->registerApiHandler("/api/v1/servers/localhost", apiServerDetail, "GET");
2746 d_ws->registerApiHandler("/api/v1/servers", apiServer, "GET");
2747 d_ws->registerApiHandler("/api/v1", apiDiscoveryV1, "GET");
2748 d_ws->registerApiHandler("/api/docs", apiDocs, "GET");
2749 d_ws->registerApiHandler("/api", apiDiscovery, "GET");
2750 }
2751 if (::arg().mustDo("webserver")) {
2752 d_ws->registerWebHandler(
2753 "/style.css", [](HttpRequest* req, HttpResponse* resp) { cssfunction(req, resp); }, "GET");
2754 d_ws->registerWebHandler(
2755 "/", [this](HttpRequest* req, HttpResponse* resp) { indexfunction(req, resp); }, "GET");
2756 d_ws->registerWebHandler("/metrics", prometheusMetrics, "GET");
2757 }
2758 d_ws->go();
2759 }
2760 catch (...) {
2761 g_log << Logger::Error << "AuthWebServer thread caught an exception, dying" << endl;
2762 _exit(1);
2763 }
2764 }