]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-auth.cc
Merge pull request #13768 from rgacogne/ddist-maintenance-hook
[thirdparty/pdns.git] / pdns / ws-auth.cc
CommitLineData
12c86877 1/*
6edbf68a
PL
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 */
7330f904
FM
22#include "dnsbackend.hh"
23#include "webserver.hh"
ad528718 24#include <array>
870a0fe4
AT
25#ifdef HAVE_CONFIG_H
26#include "config.h"
27#endif
9054d8a4 28#include "utility.hh"
d267d1bf 29#include "dynlistener.hh"
2470b36e 30#include "ws-auth.hh"
e611a06c 31#include "json.hh"
12c86877
BH
32#include "logger.hh"
33#include "statbag.hh"
34#include "misc.hh"
6f16f97c 35#include "base64.hh"
12c86877
BH
36#include "arguments.hh"
37#include "dns.hh"
6cc98ddf 38#include "comment.hh"
e611a06c 39#include "ueberbackend.hh"
dcc65f25 40#include <boost/format.hpp>
fa8fd4d2 41
9ac4a7c6 42#include "namespaces.hh"
6ec5e728 43#include "ws-api.hh"
ba1a571d 44#include "version.hh"
d29d5db7 45#include "dnsseckeeper.hh"
3c3c006b 46#include <iomanip>
0f0e73fe 47#include "zoneparser-tng.hh"
8cb70f23 48#include "auth-main.hh"
bf269e28 49#include "auth-caches.hh"
f4922e19 50#include "auth-zonecache.hh"
519f5484 51#include "threadname.hh"
ac5298aa 52#include "tsigutils.hh"
8537b9f0 53
24afabad 54using json11::Json;
12c86877 55
2c03bdc8
FM
56Ewma::Ewma() { dt.set(); }
57
58void 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
72double Ewma::get10() const
73{
74 return d_10;
75}
76
77double Ewma::get5() const
78{
79 return d_5;
80}
81
82double Ewma::get1() const
83{
84 return d_1;
85}
86
87double Ewma::getMax() const
88{
89 return d_max;
90}
91
efc47b04 92static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp);
f63168e6 93
646bcd7d 94// QTypes that MUST NOT have multiple records of the same type in a given RRset.
212f57b8 95static const std::set<uint16_t> onlyOneEntryTypes = {QType::CNAME, QType::DNAME, QType::SOA};
646bcd7d 96// QTypes that MUST NOT be used with any other QType on the same name.
212f57b8 97static const std::set<uint16_t> exclusiveEntryTypes = {QType::CNAME};
2eb206ec 98// QTypes that MUST be at apex.
c9767646 99static const std::set<uint16_t> atApexTypes = {QType::SOA, QType::DNSKEY};
2eb206ec
KM
100// QTypes that are NOT allowed at apex.
101static const std::set<uint16_t> nonApexTypes = {QType::DS};
646bcd7d 102
8a70e507 103AuthWebServer::AuthWebServer() :
eace2c24 104 d_start(time(nullptr)),
8a70e507
CHB
105 d_min10(0),
106 d_min5(0),
eace2c24 107 d_min1(0)
12c86877 108{
64c4f83c 109 if (arg().mustDo("webserver") || arg().mustDo("api")) {
2bbc9eb0 110 d_ws = std::make_unique<WebServer>(arg()["webserver-address"], arg().asNum("webserver-port"));
64c4f83c
RG
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"));
8ca656a8 113 d_ws->setLogLevel(arg()["webserver-loglevel"]);
0010aefa
PL
114
115 NetmaskGroup acl;
116 acl.toMasks(::arg()["webserver-allow-from"]);
117 d_ws->setACL(acl);
118
214b034e
PD
119 d_ws->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
120
825fa717
CH
121 d_ws->bind();
122 }
12c86877
BH
123}
124
a690b08c 125void AuthWebServer::go(StatBag& stats)
12c86877 126{
536ab56f 127 S.doRings();
212f57b8 128 std::thread webT([this]() { webThread(); });
0ddde5fb 129 webT.detach();
a690b08c 130 std::thread statT([this, &stats]() { statThread(stats); });
0ddde5fb 131 statT.detach();
12c86877
BH
132}
133
a690b08c 134void AuthWebServer::statThread(StatBag& stats)
12c86877
BH
135{
136 try {
519f5484 137 setThreadName("pdns/statHelper");
212f57b8 138 for (;;) {
a690b08c
FM
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")));
12c86877
BH
144 Utility::sleep(1);
145 }
146 }
212f57b8
FM
147 catch (...) {
148 g_log << Logger::Error << "Webserver statThread caught an exception, dying" << endl;
5bd2ea7b 149 _exit(1);
12c86877
BH
150 }
151}
152
ad528718 153static string htmlescape(const string& inputString)
212f57b8 154{
9f3fdaa0 155 string result;
ad528718
FM
156 for (char currentChar : inputString) {
157 switch (currentChar) {
9f3fdaa0 158 case '&':
c86a96f9 159 result += "&amp;";
9f3fdaa0
CH
160 break;
161 case '<':
162 result += "&lt;";
163 break;
164 case '>':
165 result += "&gt;";
166 break;
c7f59d62
PL
167 case '"':
168 result += "&quot;";
169 break;
9f3fdaa0 170 default:
ad528718 171 result += currentChar;
9f3fdaa0
CH
172 }
173 }
174 return result;
175}
176
212f57b8 177static void printtable(ostringstream& ret, const string& ringname, const string& title, int limit = 10)
12c86877 178{
ad528718 179 unsigned int tot = 0;
212f57b8
FM
180 int entries = 0;
181 vector<pair<string, unsigned int>> ring = S.getRing(ringname);
12c86877 182
ad528718
FM
183 for (const auto& entry : ring) {
184 tot += entry.second;
12c86877
BH
185 entries++;
186 }
187
212f57b8
FM
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: ";
ad528718
FM
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]) {
212f57b8 197 ret << "<a href=\"?resizering=" << htmlescape(ringname) << "&amp;size=" << sizes[i] << "\">" << sizes[i] << "</a> ";
ad528718
FM
198 }
199 else {
212f57b8 200 ret << "(" << sizes[i] << ") ";
ad528718 201 }
12c86877 202 }
212f57b8 203 ret << "</span></div>";
12c86877 204
212f57b8 205 ret << "<table class=\"data\">";
ad528718
FM
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) {
212f57b8
FM
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;
12c86877 211 }
212f57b8 212 ret << "<tr><td colspan=3></td></tr>" << endl;
ad528718 213 if (printed != tot) {
212f57b8 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;
ad528718 215 }
12c86877 216
212f57b8
FM
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;
12c86877
BH
219}
220
ad528718 221static void printvars(ostringstream& ret)
12c86877 222{
212f57b8 223 ret << "<div class=panel><h2>Variables</h2><table class=\"data\">" << endl;
12c86877 224
212f57b8
FM
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;
12c86877 228 }
e2a77e08 229
212f57b8 230 ret << "</table></div>" << endl;
12c86877
BH
231}
232
ad528718 233static void printargs(ostringstream& ret)
12c86877 234{
212f57b8 235 ret << R"(<table border=1><tr><td colspan=3 bgcolor="#0000ff"><font color="#ffffff">Arguments</font></td>)" << endl;
12c86877 236
212f57b8
FM
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;
12c86877
BH
240 }
241}
242
dea47634 243string AuthWebServer::makePercentage(const double& val)
b6f57093
BH
244{
245 return (boost::format("%.01f%%") % val).str();
246}
247
dea47634 248void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
12c86877 249{
212f57b8 250 if (!req->getvars["resetring"].empty()) {
ad528718 251 if (S.ringExists(req->getvars["resetring"])) {
583ea80d 252 S.resetRing(req->getvars["resetring"]);
ad528718 253 }
d7b8730e 254 resp->status = 302;
0665b7e6 255 resp->headers["Location"] = req->url.path;
80d59cd1 256 return;
12c86877 257 }
212f57b8
FM
258 if (!req->getvars["resizering"].empty()) {
259 int size = std::stoi(req->getvars["size"]);
ad528718 260 if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000) {
335da0ba 261 S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
ad528718 262 }
d7b8730e 263 resp->status = 302;
0665b7e6 264 resp->headers["Location"] = req->url.path;
80d59cd1 265 return;
12c86877
BH
266 }
267
268 ostringstream ret;
269
212f57b8
FM
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
ad528718 292 if (d_cachemisses.get10() + d_cachehits.get10() > 0) {
212f57b8 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;
ad528718 294 }
212f57b8 295
ad528718 296 if (d_qcachemisses.get10() + d_qcachehits.get10() > 0) {
212f57b8 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;
ad528718 298 }
212f57b8
FM
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
ad528718 302 ret << "Total queries: " << S.read("udp-queries") << ". Question/answer latency: " << static_cast<double>(S.read("latency")) / 1000.0 << "ms</p><br>" << endl;
212f57b8 303 if (req->getvars["ring"].empty()) {
eb029b8e 304 auto entries = S.listRings();
ad528718
FM
305 for (const auto& entry : entries) {
306 printtable(ret, entry, S.getRingTitle(entry));
eb029b8e 307 }
12c86877 308
f6154a3b 309 printvars(ret);
ad528718 310 if (arg().mustDo("webserver-print-arguments")) {
f6154a3b 311 printargs(ret);
ad528718 312 }
12c86877 313 }
ad528718 314 else if (S.ringExists(req->getvars["ring"])) {
212f57b8 315 printtable(ret, req->getvars["ring"], S.getRingTitle(req->getvars["ring"]), 100);
ad528718 316 }
12c86877 317
212f57b8
FM
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;
12c86877 321
80d59cd1 322 resp->body = ret.str();
61f5d289 323 resp->status = 200;
12c86877
BH
324}
325
1d6b70f9 326/** Helper to build a record content as needed. */
212f57b8
FM
327static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot)
328{
1d6b70f9 329 // noDot: for backend storage, pass true. for API users, pass false.
d525b58b 330 auto drc = DNSRecordContent::make(qtype.getCode(), QClass::IN, content);
7fe1a82b 331 return drc->getZoneRepresentation(noDot);
1d6b70f9
CH
332}
333
334/** "Normalize" record content for API consumers. */
212f57b8
FM
335static inline string makeApiRecordContent(const QType& qtype, const string& content)
336{
1d6b70f9
CH
337 return makeRecordContent(qtype, content, false);
338}
339
340/** "Normalize" record content for backend storage. */
212f57b8
FM
341static inline string makeBackendRecordContent(const QType& qtype, const string& content)
342{
1d6b70f9
CH
343 return makeRecordContent(qtype, content, true);
344}
345
ad528718 346static Json::object getZoneInfo(const DomainInfo& domainInfo, DNSSECKeeper* dnssecKeeper)
212f57b8 347{
ad528718 348 string zoneId = apiZoneNameToId(domainInfo.zone);
d525b58b 349 vector<string> primaries;
ad528718
FM
350 primaries.reserve(domainInfo.primaries.size());
351 for (const auto& primary : domainInfo.primaries) {
352 primaries.push_back(primary.toStringWithPortExcept(53));
c8b929d9 353 }
24ded6cc 354
a0930e45 355 auto obj = Json::object{
62a9a74c 356 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
a0930e45
KM
357 {"id", zoneId},
358 {"url", "/api/v1/servers/localhost/zones/" + zoneId},
ad528718
FM
359 {"name", domainInfo.zone.toString()},
360 {"kind", domainInfo.getKindString()},
361 {"catalog", (!domainInfo.catalog.empty() ? domainInfo.catalog.toString() : "")},
362 {"account", domainInfo.account},
d525b58b 363 {"masters", std::move(primaries)},
ad528718
FM
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);
cf0541a3 369 string soa_edit;
ad528718
FM
370 dnssecKeeper->getSoaEdit(domainInfo.zone, soa_edit, false);
371 obj["edited_serial"] = (double)calculateEditSOA(domainInfo.serial, soa_edit, domainInfo.zone);
07a32d18
CH
372 }
373 return obj;
c04b5870
CH
374}
375
212f57b8
FM
376static bool shouldDoRRSets(HttpRequest* req)
377{
ad528718 378 if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true") {
986e4858 379 return true;
ad528718
FM
380 }
381 if (req->getvars["rrsets"] == "false") {
986e4858 382 return false;
ad528718
FM
383 }
384
212f57b8 385 throw ApiException("'rrsets' request parameter value '" + req->getvars["rrsets"] + "' is not supported");
986e4858
PL
386}
387
ad528718 388static void fillZone(UeberBackend& backend, const DNSName& zonename, HttpResponse* resp, HttpRequest* req)
212f57b8 389{
ad528718
FM
390 DomainInfo domainInfo;
391
392 if (!backend.getDomainInfo(zonename, domainInfo)) {
77bfe8de
PL
393 throw HttpNotFoundException();
394 }
1abb81f4 395
ad528718
FM
396 DNSSECKeeper dnssecKeeper(&backend);
397 Json::object doc = getZoneInfo(domainInfo, &dnssecKeeper);
62a9a74c 398 // extra stuff getZoneInfo doesn't do for us (more expensive)
d29d5db7 399 string soa_edit_api;
ad528718 400 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
62a9a74c 401 doc["soa_edit_api"] = soa_edit_api;
6bb25159 402 string soa_edit;
ad528718 403 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
62a9a74c 404 doc["soa_edit"] = soa_edit;
efc52697 405
986e4858 406 string nsec3param;
986e4858 407 bool nsec3narrowbool = false;
ad528718 408 bool is_secured = dnssecKeeper.isSecuredZone(zonename);
efc52697 409 if (is_secured) { // ignore NSEC3PARAM and NSEC3NARROW metadata present in the db for unsigned zones
ad528718 410 domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
efc52697 411 string nsec3narrow;
ad528718 412 domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
efc52697
KM
413 if (nsec3narrow == "1") {
414 nsec3narrowbool = true;
415 }
416 }
417 doc["nsec3param"] = nsec3param;
986e4858 418 doc["nsec3narrow"] = nsec3narrowbool;
efc52697 419 doc["dnssec"] = is_secured;
986e4858
PL
420
421 string api_rectify;
ad528718 422 domainInfo.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
986e4858
PL
423 doc["api_rectify"] = (api_rectify == "1");
424
eecc9ed5 425 // TSIG
ad528718
FM
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);
eecc9ed5 430
d525b58b
KM
431 Json::array tsig_primary_keys;
432 for (const auto& keyname : tsig_primary) {
ad528718 433 tsig_primary_keys.emplace_back(apiZoneNameToId(DNSName(keyname)));
eecc9ed5 434 }
d525b58b 435 doc["master_tsig_key_ids"] = tsig_primary_keys;
eecc9ed5 436
c02c999b
KM
437 Json::array tsig_secondary_keys;
438 for (const auto& keyname : tsig_secondary) {
ad528718 439 tsig_secondary_keys.emplace_back(apiZoneNameToId(DNSName(keyname)));
eecc9ed5 440 }
c02c999b 441 doc["slave_tsig_key_ids"] = tsig_secondary_keys;
eecc9ed5 442
de7fd3c6 443 if (shouldDoRRSets(req)) {
986e4858
PL
444 vector<DNSResourceRecord> records;
445 vector<Comment> comments;
446
447 // load all records + sort
448 {
ad528718 449 DNSResourceRecord resourceRecord;
486210b8 450 if (req->getvars.count("rrset_name") == 0) {
ad528718 451 domainInfo.backend->list(zonename, static_cast<int>(domainInfo.id), true); // incl. disabled
212f57b8
FM
452 }
453 else {
ad528718 454 QType qType;
486210b8 455 if (req->getvars.count("rrset_type") == 0) {
ad528718 456 qType = QType::ANY;
212f57b8
FM
457 }
458 else {
ad528718 459 qType = req->getvars["rrset_type"];
486210b8 460 }
ad528718 461 domainInfo.backend->lookup(qType, DNSName(req->getvars["rrset_name"]), static_cast<int>(domainInfo.id));
486210b8 462 }
ad528718
FM
463 while (domainInfo.backend->get(resourceRecord)) {
464 if (resourceRecord.qtype.getCode() == 0) {
986e4858 465 continue; // skip empty non-terminals
ad528718
FM
466 }
467 records.push_back(resourceRecord);
986e4858 468 }
ad528718 469 sort(records.begin(), records.end(), [](const DNSResourceRecord& rrA, const DNSResourceRecord& rrB) {
212f57b8
FM
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 */
ad528718
FM
473 if (rrA.qname == rrB.qname) {
474 return rrB.qtype < rrA.qtype;
212f57b8 475 }
ad528718 476 return rrB.qname < rrA.qname;
212f57b8 477 });
6754ef71 478 }
6754ef71 479
986e4858
PL
480 // load all comments + sort
481 {
482 Comment comment;
ad528718
FM
483 domainInfo.backend->listComments(domainInfo.id);
484 while (domainInfo.backend->getComment(comment)) {
986e4858
PL
485 comments.push_back(comment);
486 }
ad528718 487 sort(comments.begin(), comments.end(), [](const Comment& rrA, const Comment& rrB) {
212f57b8
FM
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 */
ad528718
FM
491 if (rrA.qname == rrB.qname) {
492 return rrB.qtype < rrA.qtype;
212f57b8 493 }
ad528718 494 return rrB.qname < rrA.qname;
212f57b8 495 });
6754ef71 496 }
6754ef71 497
986e4858
PL
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;
ad528718 504 uint32_t ttl = 0;
986e4858
PL
505 auto rit = records.begin();
506 auto cit = comments.begin();
507
508 while (rit != records.end() || cit != comments.end()) {
5481c77c
PD
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))) {
986e4858
PL
511 current_qname = rit->qname;
512 current_qtype = rit->qtype;
513 ttl = rit->ttl;
212f57b8
FM
514 }
515 else {
986e4858
PL
516 current_qname = cit->qname;
517 current_qtype = cit->qtype;
518 ttl = 0;
519 }
6754ef71 520
212f57b8 521 while (rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
986e4858 522 ttl = min(ttl, rit->ttl);
212f57b8
FM
523 rrset_records.push_back(Json::object{
524 {"disabled", rit->disabled},
525 {"content", makeApiRecordContent(rit->qtype, rit->content)}});
986e4858
PL
526 rit++;
527 }
528 while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
212f57b8
FM
529 rrset_comments.push_back(Json::object{
530 {"modified_at", (double)cit->modified_at},
531 {"account", cit->account},
532 {"content", cit->content}});
986e4858
PL
533 cit++;
534 }
535
536 rrset["name"] = current_qname.toString();
d5fcd583 537 rrset["type"] = current_qtype.toString();
986e4858
PL
538 rrset["records"] = rrset_records;
539 rrset["comments"] = rrset_comments;
540 rrset["ttl"] = (double)ttl;
ad528718 541 rrsets.emplace_back(rrset);
986e4858
PL
542 rrset.clear();
543 rrset_records.clear();
544 rrset_comments.clear();
6754ef71
CH
545 }
546
986e4858 547 doc["rrsets"] = rrsets;
6754ef71
CH
548 }
549
917f686a 550 resp->setJsonBody(doc);
1abb81f4
CH
551}
552
212f57b8 553void productServerStatisticsFetch(map<string, string>& out)
6ec5e728 554{
a45303b8 555 vector<string> items = S.getEntries();
212f57b8 556 for (const string& item : items) {
335da0ba 557 out[item] = std::to_string(S.read(item));
a45303b8
CH
558 }
559
560 // add uptime
c509c9fa 561 out["uptime"] = std::to_string(time(nullptr) - g_starttime);
c67bf8c5
CH
562}
563
1a09e47b 564std::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
5376a5d7
RG
565{
566 try {
567 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
568 return S.read(name);
569 }
1a09e47b
RG
570 catch (...) {
571 return std::nullopt;
5376a5d7
RG
572 }
573}
574
ad528718 575static void validateGatheredRRType(const DNSResourceRecord& resourceRecord)
212f57b8 576{
ad528718
FM
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");
24ded6cc
CHB
579 }
580}
581
212f57b8
FM
582static void gatherRecords(const Json& container, const DNSName& qname, const QType& qtype, const uint32_t ttl, vector<DNSResourceRecord>& new_records)
583{
ad528718
FM
584 DNSResourceRecord resourceRecord;
585 resourceRecord.qname = qname;
586 resourceRecord.qtype = qtype;
587 resourceRecord.auth = true;
588 resourceRecord.ttl = ttl;
24ded6cc 589
ad528718 590 validateGatheredRRType(resourceRecord);
7f201325 591 const auto& items = container["records"].array_items();
212f57b8 592 for (const auto& record : items) {
1f68b185 593 string content = stringFromJson(record, "content");
36852ff8
PD
594 if (record.object_items().count("priority") > 0) {
595 throw std::runtime_error("`priority` element is not allowed in record");
596 }
ad528718 597 resourceRecord.disabled = false;
212f57b8 598 if (!record["disabled"].is_null()) {
ad528718 599 resourceRecord.disabled = boolFromJson(record, "disabled");
70aad565 600 }
1f68b185 601
1f68b185
CH
602 // validate that the client sent something we can actually parse, and require that data to be dotted.
603 try {
ad528718
FM
604 if (resourceRecord.qtype.getCode() != QType::AAAA) {
605 string tmp = makeApiRecordContent(resourceRecord.qtype, content);
1f68b185 606 if (!pdns_iequals(tmp, content)) {
212f57b8 607 throw std::runtime_error("Not in expected format (parsed as '" + tmp + "')");
1f68b185 608 }
212f57b8
FM
609 }
610 else {
ad528718
FM
611 struct in6_addr tmpbuf
612 {
613 };
1f68b185
CH
614 if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
615 throw std::runtime_error("Invalid IPv6 address");
1e5b9ab9 616 }
f63168e6 617 }
ad528718 618 resourceRecord.content = makeBackendRecordContent(resourceRecord.qtype, content);
1f68b185 619 }
212f57b8 620 catch (std::exception& e) {
ad528718 621 throw ApiException("Record " + resourceRecord.qname.toString() + "/" + resourceRecord.qtype.toString() + " '" + content + "': " + e.what());
1f68b185 622 }
f63168e6 623
ad528718 624 new_records.push_back(resourceRecord);
f63168e6
CH
625 }
626}
627
212f57b8
FM
628static void gatherComments(const Json& container, const DNSName& qname, const QType& qtype, vector<Comment>& new_comments)
629{
ad528718
FM
630 Comment comment;
631 comment.qname = qname;
632 comment.qtype = qtype;
f63168e6 633
4646277d 634 time_t now = time(nullptr);
ad528718 635 for (const auto& currentComment : container["comments"].array_items()) {
cf17e6b8 636 // FIXME 2036 issue internally in uintFromJson
ad528718
FM
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);
f63168e6
CH
641 }
642}
6cc98ddf 643
212f57b8
FM
644static void checkDefaultDNSSECAlgos()
645{
986e4858
PL
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
ad528718
FM
652 if (!::arg()["default-zsk-algorithm"].empty()) {
653 if (k_algo == -1) {
986e4858 654 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
ad528718
FM
655 }
656 if (k_algo <= 10 && k_size == 0) {
212f57b8 657 throw ApiException("default-ksk-algorithm is set to an algorithm(" + ::arg()["default-ksk-algorithm"] + ") that requires a non-zero default-ksk-size!");
ad528718 658 }
986e4858
PL
659 }
660
ad528718
FM
661 if (!::arg()["default-zsk-algorithm"].empty()) {
662 if (z_algo == -1) {
986e4858 663 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
ad528718
FM
664 }
665 if (z_algo <= 10 && z_size == 0) {
212f57b8 666 throw ApiException("default-zsk-algorithm is set to an algorithm(" + ::arg()["default-zsk-algorithm"] + ") that requires a non-zero default-zsk-size!");
ad528718 667 }
986e4858
PL
668 }
669}
670
212f57b8
FM
671static void throwUnableToSecure(const DNSName& zonename)
672{
89a7e706 673 throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
212f57b8 674 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
89a7e706
PL
675}
676
8ea32ea1
CH
677/*
678 * Add KSK and ZSK to an existing zone. Algorithms and sizes will be chosen per configuration.
212f57b8 679 */
ad528718 680static void addDefaultDNSSECKeys(DNSSECKeeper& dnssecKeeper, const DNSName& zonename)
212f57b8 681{
8ea32ea1
CH
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) {
ad528718
FM
689 int64_t keyID{-1};
690 if (!dnssecKeeper.addKey(zonename, true, k_algo, keyID, k_size)) {
8ea32ea1
CH
691 throwUnableToSecure(zonename);
692 }
693 }
694
695 if (z_algo != -1) {
ad528718
FM
696 int64_t keyID{-1};
697 if (!dnssecKeeper.addKey(zonename, false, z_algo, keyID, z_size)) {
8ea32ea1
CH
698 throwUnableToSecure(zonename);
699 }
700 }
701}
702
ad528718 703static bool isZoneApiRectifyEnabled(const DomainInfo& domainInfo)
212f57b8 704{
d71d2c88 705 string api_rectify;
ad528718 706 domainInfo.backend->getDomainMetadataOne(domainInfo.zone, "API-RECTIFY", api_rectify);
d71d2c88
CH
707 if (api_rectify.empty() && ::arg().mustDo("default-api-rectify")) {
708 api_rectify = "1";
709 }
710 return api_rectify == "1";
711}
712
d525b58b 713static void extractDomainInfoFromDocument(const Json& document, boost::optional<DomainInfo::DomainKind>& kind, boost::optional<vector<ComboAddress>>& primaries, boost::optional<DNSName>& catalog, boost::optional<string>& account)
a0930e45 714{
4c70ffb3
CH
715 if (document["kind"].is_string()) {
716 kind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
212f57b8
FM
717 }
718 else {
4c70ffb3
CH
719 kind = boost::none;
720 }
721
a190938f 722 if (document["masters"].is_array()) {
d525b58b 723 primaries = vector<ComboAddress>();
212f57b8 724 for (const auto& value : document["masters"].array_items()) {
d525b58b 725 string primary = value.string_value();
ad528718 726 if (primary.empty()) {
d525b58b 727 throw ApiException("Primary can not be an empty string");
ad528718 728 }
a190938f 729 try {
d525b58b 730 primaries->emplace_back(primary, 53);
212f57b8
FM
731 }
732 catch (const PDNSException& e) {
d525b58b 733 throw ApiException("Primary (" + primary + ") is not an IP address: " + e.reason);
a190938f 734 }
27efa4be 735 }
212f57b8
FM
736 }
737 else {
d525b58b 738 primaries = boost::none;
986e4858 739 }
4c70ffb3 740
a0930e45
KM
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
4c70ffb3
CH
749 if (document["account"].is_string()) {
750 account = document["account"].string_value();
212f57b8
FM
751 }
752 else {
4c70ffb3 753 account = boost::none;
986e4858 754 }
4c70ffb3
CH
755}
756
c67dfcb4
CH
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
212f57b8 761 */
ad528718 762static void extractJsonTSIGKeyIds(UeberBackend& backend, const Json& jsonArray, vector<string>& metadata)
212f57b8
FM
763{
764 for (const auto& value : jsonArray.array_items()) {
c67dfcb4
CH
765 auto keyname(apiZoneIdToName(value.string_value()));
766 DNSName keyAlgo;
767 string keyContent;
ad528718 768 if (!backend.getTSIGKey(keyname, keyAlgo, keyContent)) {
212f57b8 769 throw ApiException("A TSIG key with the name '" + keyname.toLogString() + "' does not exist");
c67dfcb4
CH
770 }
771 metadata.push_back(keyname.toString());
772 }
773}
774
002de4d7 775// Must be called within backend transaction.
ad528718 776static void updateDomainSettingsFromDocument(UeberBackend& backend, DomainInfo& domainInfo, const DNSName& zonename, const Json& document, bool zoneWasModified)
212f57b8 777{
4c70ffb3 778 boost::optional<DomainInfo::DomainKind> kind;
d525b58b 779 boost::optional<vector<ComboAddress>> primaries;
a0930e45 780 boost::optional<DNSName> catalog;
a190938f 781 boost::optional<string> account;
4c70ffb3 782
d525b58b 783 extractDomainInfoFromDocument(document, kind, primaries, catalog, account);
4c70ffb3
CH
784
785 if (kind) {
ad528718
FM
786 domainInfo.backend->setKind(zonename, *kind);
787 domainInfo.kind = *kind;
4c70ffb3 788 }
d525b58b 789 if (primaries) {
ad528718 790 domainInfo.backend->setPrimaries(zonename, *primaries);
4c70ffb3 791 }
a0930e45 792 if (catalog) {
ad528718 793 domainInfo.backend->setCatalog(zonename, *catalog);
a0930e45 794 }
4c70ffb3 795 if (account) {
ad528718 796 domainInfo.backend->setAccount(zonename, *account);
4c70ffb3
CH
797 }
798
1f68b185 799 if (document["soa_edit_api"].is_string()) {
ad528718 800 domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
d29d5db7 801 }
1f68b185 802 if (document["soa_edit"].is_string()) {
ad528718 803 domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
6bb25159 804 }
c00908f1
PL
805 try {
806 bool api_rectify = boolFromJson(document, "api_rectify");
ad528718 807 domainInfo.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0");
986e4858 808 }
212f57b8
FM
809 catch (const JsonException&) {
810 }
986e4858 811
ad528718 812 DNSSECKeeper dnssecKeeper(&backend);
168a76b3 813 bool shouldRectify = zoneWasModified;
986e4858
PL
814 bool dnssecInJSON = false;
815 bool dnssecDocVal = false;
df434f42 816 bool nsec3paramInJSON = false;
efc52697 817 bool updateNsec3Param = false;
df434f42 818 string nsec3paramDocVal;
986e4858
PL
819
820 try {
821 dnssecDocVal = boolFromJson(document, "dnssec");
822 dnssecInJSON = true;
823 }
212f57b8
FM
824 catch (const JsonException&) {
825 }
986e4858 826
df434f42
PL
827 try {
828 nsec3paramDocVal = stringFromJson(document, "nsec3param");
829 nsec3paramInJSON = true;
830 }
212f57b8
FM
831 catch (const JsonException&) {
832 }
df434f42 833
ad528718
FM
834 bool isDNSSECZone = dnssecKeeper.isSecuredZone(zonename);
835 bool isPresigned = dnssecKeeper.isPresigned(zonename);
986e4858
PL
836
837 if (dnssecInJSON) {
838 if (dnssecDocVal) {
839 if (!isDNSSECZone) {
ad528718 840 addDefaultDNSSECKeys(dnssecKeeper, zonename);
986e4858
PL
841
842 // Used later for NSEC3PARAM
ad528718 843 isDNSSECZone = dnssecKeeper.isSecuredZone(zonename);
986e4858
PL
844
845 if (!isDNSSECZone) {
89a7e706 846 throwUnableToSecure(zonename);
986e4858
PL
847 }
848 shouldRectify = true;
efc52697 849 updateNsec3Param = true;
986e4858 850 }
212f57b8
FM
851 }
852 else {
986e4858
PL
853 // "dnssec": false in json
854 if (isDNSSECZone) {
ad528718
FM
855 string info;
856 string error;
857 if (!dnssecKeeper.unSecureZone(zonename, error)) {
212f57b8 858 throw ApiException("Error while un-securing zone '" + zonename.toString() + "': " + error);
cbe8b186 859 }
ad528718 860 isDNSSECZone = dnssecKeeper.isSecuredZone(zonename, false);
cbe8b186 861 if (isDNSSECZone) {
212f57b8 862 throw ApiException("Unable to un-secure zone '" + zonename.toString() + "'");
cbe8b186
PL
863 }
864 shouldRectify = true;
efc52697 865 updateNsec3Param = true;
986e4858
PL
866 }
867 }
868 }
869
efc52697 870 if (nsec3paramInJSON || updateNsec3Param) {
986e4858 871 shouldRectify = true;
efc52697
KM
872 if (!isDNSSECZone && !nsec3paramDocVal.empty()) {
873 throw ApiException("NSEC3PARAM value provided for zone '" + zonename.toString() + "', but zone is not DNSSEC secured.");
986e4858 874 }
df434f42 875
efc52697 876 if (nsec3paramDocVal.empty()) {
df434f42 877 // Switch to NSEC
ad528718 878 if (!dnssecKeeper.unsetNSEC3PARAM(zonename)) {
df434f42
PL
879 throw ApiException("Unable to remove NSEC3PARAMs from zone '" + zonename.toString());
880 }
986e4858 881 }
efc52697 882 else {
df434f42
PL
883 // Set the NSEC3PARAMs
884 NSEC3PARAMRecordContent ns3pr(nsec3paramDocVal);
ad528718
FM
885 string error_msg;
886 if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) {
212f57b8 887 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg);
df434f42 888 }
ad528718 889 if (!dnssecKeeper.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
212f57b8 890 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' passed our basic sanity checks, but cannot be used with the current backend.");
df434f42 891 }
986e4858
PL
892 }
893 }
894
cf0541a3 895 if (shouldRectify && !isPresigned) {
a843c67e 896 // Rectify
ad528718 897 if (isZoneApiRectifyEnabled(domainInfo)) {
a843c67e
KM
898 string info;
899 string error_msg;
ad528718 900 if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false) && !domainInfo.isSecondaryType()) {
26636b05 901 // for Secondary zones, it is possible that rectifying was not needed (example: empty zone).
a843c67e
KM
902 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
903 }
904 }
905
906 // Increase serial
907 string soa_edit_api_kind;
ad528718 908 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
a843c67e 909 if (!soa_edit_api_kind.empty()) {
ad528718
FM
910 SOAData soaData;
911 if (!backend.getSOAUncached(zonename, soaData)) {
a843c67e 912 return;
ad528718 913 }
a843c67e
KM
914
915 string soa_edit_kind;
ad528718 916 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
a843c67e 917
ad528718
FM
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))) {
a843c67e
KM
921 throw ApiException("Hosting backend does not support editing records.");
922 }
923 }
a843c67e 924 }
986e4858 925 }
1096d493
PL
926
927 if (!document["master_tsig_key_ids"].is_null()) {
928 vector<string> metadata;
ad528718
FM
929 extractJsonTSIGKeyIds(backend, document["master_tsig_key_ids"], metadata);
930 if (!domainInfo.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) {
d525b58b 931 throw HttpInternalServerErrorException("Unable to set new TSIG primary keys for zone '" + zonename.toLogString() + "'");
1096d493
PL
932 }
933 }
934 if (!document["slave_tsig_key_ids"].is_null()) {
935 vector<string> metadata;
ad528718
FM
936 extractJsonTSIGKeyIds(backend, document["slave_tsig_key_ids"], metadata);
937 if (!domainInfo.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) {
c02c999b 938 throw HttpInternalServerErrorException("Unable to set new TSIG secondary keys for zone '" + zonename.toLogString() + "'");
1096d493
PL
939 }
940 }
bb9fd223
CH
941}
942
212f57b8
FM
943static bool isValidMetadataKind(const string& kind, bool readonly)
944{
945 static vector<string> builtinOptions{
24e11043
CJ
946 "ALLOW-AXFR-FROM",
947 "AXFR-SOURCE",
948 "ALLOW-DNSUPDATE-FROM",
949 "TSIG-ALLOW-DNSUPDATE",
950 "FORWARD-DNSUPDATE",
951 "SOA-EDIT-DNSUPDATE",
4c5b6925 952 "NOTIFY-DNSUPDATE",
24e11043
CJ
953 "ALSO-NOTIFY",
954 "AXFR-MASTER-TSIG",
b08f1315
OM
955 "GSS-ALLOW-AXFR-PRINCIPAL",
956 "GSS-ACCEPTOR-PRINCIPAL",
24e11043
CJ
957 "IXFR",
958 "LUA-AXFR-SCRIPT",
959 "NSEC3NARROW",
960 "NSEC3PARAM",
961 "PRESIGNED",
962 "PUBLISH-CDNSKEY",
963 "PUBLISH-CDS",
c750c26e 964 "SLAVE-RENOTIFY",
24e11043
CJ
965 "SOA-EDIT",
966 "TSIG-ALLOW-AXFR",
1dd16193
FM
967 "TSIG-ALLOW-DNSUPDATE",
968 };
24e11043
CJ
969
970 // the following options do not allow modifications via API
212f57b8 971 static vector<string> protectedOptions{
986e4858 972 "API-RECTIFY",
2fabf5ce 973 "AXFR-MASTER-TSIG",
24e11043
CJ
974 "NSEC3NARROW",
975 "NSEC3PARAM",
976 "PRESIGNED",
2fabf5ce 977 "LUA-AXFR-SCRIPT",
1dd16193
FM
978 "TSIG-ALLOW-AXFR",
979 };
24e11043 980
ad528718 981 if (kind.find("X-") == 0) {
9ac4e6d5 982 return true;
ad528718 983 }
9ac4e6d5 984
24e11043
CJ
985 bool found = false;
986
ad528718
FM
987 for (const string& builtinOption : builtinOptions) {
988 if (kind == builtinOption) {
989 for (const string& protectedOption : protectedOptions) {
990 if (!readonly && builtinOption == protectedOption) {
24e11043 991 return false;
ad528718 992 }
24e11043
CJ
993 }
994 found = true;
995 break;
996 }
997 }
998
999 return found;
1000}
1001
917f686a
KF
1002/* Return OpenAPI document describing the supported API.
1003 */
1004#include "apidocfiles.h"
1005
212f57b8
FM
1006void apiDocs(HttpRequest* req, HttpResponse* resp)
1007{
917f686a
KF
1008 if (req->accept_yaml) {
1009 resp->setYamlBody(g_api_swagger_yaml);
212f57b8
FM
1010 }
1011 else if (req->accept_json) {
917f686a 1012 resp->setJsonBody(g_api_swagger_json);
212f57b8
FM
1013 }
1014 else {
917f686a
KF
1015 resp->setPlainBody(g_api_swagger_yaml);
1016 }
1017}
1018
7330f904
FM
1019class ZoneData
1020{
1021public:
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
212f57b8
FM
1042static void apiZoneMetadataGET(HttpRequest* req, HttpResponse* resp)
1043{
7330f904 1044 ZoneData zoneData{req};
24e11043 1045
212f57b8 1046 map<string, vector<string>> metas;
1929764e 1047 Json::array document;
24e11043 1048
7330f904 1049 if (!zoneData.backend.getAllDomainMetadata(zoneData.zoneName, metas)) {
1929764e
AT
1050 throw HttpNotFoundException();
1051 }
24e11043 1052
1929764e
AT
1053 for (const auto& meta : metas) {
1054 Json::array entries;
1055 for (const string& value : meta.second) {
ad528718 1056 entries.emplace_back(value);
1929764e 1057 }
24e11043 1058
212f57b8
FM
1059 Json::object key{
1060 {"type", "Metadata"},
1061 {"kind", meta.first},
1062 {"metadata", entries}};
ad528718 1063 document.emplace_back(key);
1929764e
AT
1064 }
1065 resp->setJsonBody(document);
1066}
24e11043 1067
212f57b8
FM
1068static void apiZoneMetadataPOST(HttpRequest* req, HttpResponse* resp)
1069{
7330f904 1070 ZoneData zoneData{req};
24e11043 1071
1929764e
AT
1072 const auto& document = req->json();
1073 string kind;
1074 vector<string> entries;
24e11043 1075
1929764e
AT
1076 try {
1077 kind = stringFromJson(document, "kind");
212f57b8
FM
1078 }
1079 catch (const JsonException&) {
1080 throw ApiException("kind is not specified or not a string");
1929764e 1081 }
24e11043 1082
1929764e
AT
1083 if (!isValidMetadataKind(kind, false)) {
1084 throw ApiException("Unsupported metadata kind '" + kind + "'");
1085 }
24e11043 1086
1929764e 1087 vector<string> vecMetadata;
c6720e79 1088
7330f904
FM
1089 if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
1090 throw ApiException("Could not retrieve metadata entries for domain '" + zoneData.zoneName.toString() + "'");
1929764e 1091 }
c6720e79 1092
1929764e
AT
1093 const auto& metadata = document["metadata"];
1094 if (!metadata.is_array()) {
1095 throw ApiException("metadata is not specified or not an array");
1096 }
24e11043 1097
1929764e
AT
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(),
212f57b8
FM
1103 vecMetadata.cend(),
1104 value.string_value())
1105 == vecMetadata.cend()) {
1929764e 1106 vecMetadata.push_back(value.string_value());
24e11043 1107 }
1929764e
AT
1108 }
1109
7330f904
FM
1110 if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
1111 throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'");
1929764e 1112 }
24e11043 1113
7330f904 1114 DNSSECKeeper::clearMetaCache(zoneData.zoneName);
c6720e79 1115
1929764e
AT
1116 Json::array respMetadata;
1117 for (const string& value : vecMetadata) {
ad528718 1118 respMetadata.emplace_back(value);
1929764e 1119 }
d30fff94 1120
212f57b8
FM
1121 Json::object key{
1122 {"type", "Metadata"},
1123 {"kind", document["kind"]},
1124 {"metadata", respMetadata}};
c6720e79 1125
1929764e
AT
1126 resp->status = 201;
1127 resp->setJsonBody(key);
1128}
24e11043 1129
212f57b8
FM
1130static void apiZoneMetadataKindGET(HttpRequest* req, HttpResponse* resp)
1131{
7330f904 1132 ZoneData zoneData{req};
d38e81e6
PL
1133
1134 string kind = req->parameters["kind"];
24e11043 1135
a406b334
AT
1136 vector<string> metadata;
1137 Json::object document;
1138 Json::array entries;
24e11043 1139
7330f904 1140 if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, metadata)) {
a406b334
AT
1141 throw HttpNotFoundException();
1142 }
1143 if (!isValidMetadataKind(kind, true)) {
1144 throw ApiException("Unsupported metadata kind '" + kind + "'");
1145 }
24e11043 1146
a406b334
AT
1147 document["type"] = "Metadata";
1148 document["kind"] = kind;
24e11043 1149
a406b334 1150 for (const string& value : metadata) {
ad528718 1151 entries.emplace_back(value);
a406b334
AT
1152 }
1153
1154 document["metadata"] = entries;
1155 resp->setJsonBody(document);
1156}
1157
212f57b8
FM
1158static void apiZoneMetadataKindPUT(HttpRequest* req, HttpResponse* resp)
1159{
7330f904 1160 ZoneData zoneData{req};
24e11043 1161
a406b334 1162 string kind = req->parameters["kind"];
24e11043 1163
a406b334 1164 const auto& document = req->json();
24e11043 1165
a406b334
AT
1166 if (!isValidMetadataKind(kind, false)) {
1167 throw ApiException("Unsupported metadata kind '" + kind + "'");
1168 }
24e11043 1169
a406b334
AT
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");
24e11043 1178 }
a406b334
AT
1179 vecMetadata.push_back(value.string_value());
1180 }
24e11043 1181
7330f904
FM
1182 if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
1183 throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'");
a406b334 1184 }
24e11043 1185
7330f904 1186 DNSSECKeeper::clearMetaCache(zoneData.zoneName);
d30fff94 1187
212f57b8
FM
1188 Json::object key{
1189 {"type", "Metadata"},
1190 {"kind", kind},
1191 {"metadata", metadata}};
24e11043 1192
a406b334
AT
1193 resp->setJsonBody(key);
1194}
24e11043 1195
212f57b8
FM
1196static void apiZoneMetadataKindDELETE(HttpRequest* req, HttpResponse* resp)
1197{
7330f904 1198 ZoneData zoneData{req};
d30fff94 1199
a406b334
AT
1200 const string& kind = req->parameters["kind"];
1201 if (!isValidMetadataKind(kind, false)) {
1202 throw ApiException("Unsupported metadata kind '" + kind + "'");
1203 }
1204
212f57b8 1205 vector<string> metadata; // an empty vector will do it
7330f904
FM
1206 if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, metadata)) {
1207 throw ApiException("Could not delete metadata for domain '" + zoneData.zoneName.toString() + "' (" + kind + ")");
a406b334
AT
1208 }
1209
7330f904 1210 DNSSECKeeper::clearMetaCache(zoneData.zoneName);
a406b334
AT
1211 resp->status = 204;
1212}
1213
71de13d7 1214// Throws 404 if the key with inquireKeyId does not exist
ad528718 1215static void apiZoneCryptoKeysCheckKeyExists(const DNSName& zonename, int inquireKeyId, DNSSECKeeper* dnssecKeeper)
212f57b8 1216{
ad528718 1217 DNSSECKeeper::keyset_t keyset = dnssecKeeper->getKeys(zonename, false);
71de13d7 1218 bool found = false;
212f57b8
FM
1219 for (const auto& value : keyset) {
1220 if (value.second.id == (unsigned)inquireKeyId) {
71de13d7
PL
1221 found = true;
1222 break;
1223 }
1224 }
1225 if (!found) {
1226 throw HttpNotFoundException();
1227 }
1228}
1229
212f57b8
FM
1230static inline int getInquireKeyId(HttpRequest* req, const DNSName& zonename, DNSSECKeeper* dnsseckeeper)
1231{
20f8dbd7
AT
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
212f57b8
FM
1240static void apiZoneCryptokeysExport(const DNSName& zonename, int64_t inquireKeyId, HttpResponse* resp, DNSSECKeeper* dnssec_dk)
1241{
1242 DNSSECKeeper::keyset_t keyset = dnssec_dk->getKeys(zonename, false);
4b7f120a 1243
997cab68
BZ
1244 bool inquireSingleKey = inquireKeyId >= 0;
1245
24afabad 1246 Json::array doc;
212f57b8 1247 for (const auto& value : keyset) {
997cab68 1248 if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
29704f66 1249 continue;
38809e97 1250 }
24afabad 1251
b6bd795c 1252 string keyType;
60b0a236 1253 switch (value.second.keyType) {
212f57b8
FM
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()}};
24afabad 1275
2bb1f06c 1276 string publishCDS;
20f8dbd7 1277 dnssec_dk->getPublishCDS(zonename, publishCDS);
2bb1f06c
PD
1278
1279 vector<string> digestAlgos;
1280 stringtok(digestAlgos, publishCDS, ", ");
1281
1282 std::set<unsigned int> CDSalgos;
212f57b8 1283 for (auto const& digestAlgo : digestAlgos) {
a0383aad 1284 CDSalgos.insert(pdns::checked_stoi<unsigned int>(digestAlgo));
2bb1f06c
PD
1285 }
1286
b6bd795c 1287 if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
2bb1f06c 1288 Json::array cdses;
24afabad 1289 Json::array dses;
ad528718 1290 for (const uint8_t keyid : {DNSSECKeeper::DIGEST_SHA1, DNSSECKeeper::DIGEST_SHA256, DNSSECKeeper::DIGEST_GOST, DNSSECKeeper::DIGEST_SHA384}) {
997cab68 1291 try {
ad528718 1292 string dsRecordContent = makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation();
2bb1f06c 1293
ad528718 1294 dses.emplace_back(dsRecordContent);
2bb1f06c 1295
ad528718
FM
1296 if (CDSalgos.count(keyid) != 0) {
1297 cdses.emplace_back(dsRecordContent);
212f57b8
FM
1298 }
1299 }
1300 catch (...) {
1301 }
ad528718 1302 }
2bb1f06c 1303
24afabad 1304 key["ds"] = dses;
2bb1f06c 1305
ad528718 1306 if (!cdses.empty()) {
2bb1f06c
PD
1307 key["cds"] = cdses;
1308 }
4b7f120a 1309 }
29704f66
CH
1310
1311 if (inquireSingleKey) {
1312 key["privatekey"] = value.first.getKey()->convertToISC();
917f686a 1313 resp->setJsonBody(key);
29704f66
CH
1314 return;
1315 }
ad528718 1316 doc.emplace_back(key);
4b7f120a
MS
1317 }
1318
29704f66
CH
1319 if (inquireSingleKey) {
1320 // we came here because we couldn't find the requested key.
1321 throw HttpNotFoundException();
1322 }
917f686a 1323 resp->setJsonBody(doc);
20f8dbd7 1324}
997cab68 1325
212f57b8
FM
1326static void apiZoneCryptokeysGET(HttpRequest* req, HttpResponse* resp)
1327{
7330f904
FM
1328 ZoneData zoneData{req};
1329 const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
20f8dbd7 1330
7330f904 1331 apiZoneCryptokeysExport(zoneData.zoneName, inquireKeyId, resp, &zoneData.dnssecKeeper);
997cab68
BZ
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:
60b0a236 1338 * Case 1: the backend returns true on removal. This means the key is gone.
75191dc4 1339 * The server returns 204 No Content, no body.
955cbfd0 1340 * Case 2: the backend returns false on removal. An error occurred.
75191dc4
PL
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
997cab68 1344 * */
212f57b8
FM
1345static void apiZoneCryptokeysDELETE(HttpRequest* req, HttpResponse* resp)
1346{
7330f904
FM
1347 ZoneData zoneData{req};
1348 const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
20f8dbd7
AT
1349
1350 if (inquireKeyId == -1) {
212f57b8 1351 throw HttpBadRequestException();
20f8dbd7
AT
1352 }
1353
7330f904 1354 if (zoneData.dnssecKeeper.removeKey(zoneData.zoneName, inquireKeyId)) {
60b0a236 1355 resp->body = "";
75191dc4 1356 resp->status = 204;
212f57b8
FM
1357 }
1358 else {
997cab68
BZ
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 * {
5d9c6182 1367 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
997cab68
BZ
1368 * "keytype" : "ksk|zsk" <string>
1369 * "active" : "true|false" <value>
5d9c6182 1370 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
997cab68
BZ
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'"}
60b0a236
BZ
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."}
5d9c6182 1379 * Case 3: The "algorithm" isn't supported
997cab68 1380 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
60b0a236 1381 * Case 4: Algorithm <= 10 and no bits were passed
997cab68 1382 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
60b0a236
BZ
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
997cab68 1388 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
60b0a236
BZ
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
997cab68
BZ
1397 */
1398
212f57b8
FM
1399static void apiZoneCryptokeysPOST(HttpRequest* req, HttpResponse* resp)
1400{
7330f904 1401 ZoneData zoneData{req};
20f8dbd7
AT
1402
1403 const auto& document = req->json();
5d9c6182
PL
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 }
997cab68 1411 bool active = boolFromJson(document, "active", false);
33918299 1412 bool published = boolFromJson(document, "published", true);
ad528718 1413 bool keyOrZone = false;
60b0a236 1414
eefd15b3 1415 if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") {
997cab68 1416 keyOrZone = true;
212f57b8
FM
1417 }
1418 else if (stringFromJson(document, "keytype") == "zsk") {
997cab68 1419 keyOrZone = false;
212f57b8
FM
1420 }
1421 else {
997cab68
BZ
1422 throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
1423 }
1424
b727e19b 1425 int64_t insertedId = -1;
997cab68 1426
5d9c6182 1427 if (privatekey.is_null()) {
43215ca6 1428 int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
60b0a236
BZ
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");
212f57b8 1433 }
ad528718
FM
1434
1435 bits = docbits.int_value();
60b0a236 1436 }
43215ca6 1437 int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
20f8dbd7 1438 const auto& providedAlgo = document["algorithm"];
997cab68 1439 if (providedAlgo.is_string()) {
60b0a236 1440 algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
20f8dbd7 1441 if (algorithm == -1) {
997cab68 1442 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
20f8dbd7 1443 }
212f57b8
FM
1444 }
1445 else if (providedAlgo.is_number()) {
997cab68 1446 algorithm = providedAlgo.int_value();
212f57b8
FM
1447 }
1448 else if (!providedAlgo.is_null()) {
60b0a236 1449 throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
997cab68
BZ
1450 }
1451
60b0a236 1452 try {
7330f904 1453 if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, keyOrZone, algorithm, insertedId, bits, active, published)) {
b727e19b
RG
1454 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1455 }
212f57b8
FM
1456 }
1457 catch (std::runtime_error& error) {
997cab68
BZ
1458 throw ApiException(error.what());
1459 }
ad528718 1460 if (insertedId < 0) {
997cab68 1461 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
ad528718 1462 }
212f57b8
FM
1463 }
1464 else if (document["bits"].is_null() && document["algorithm"].is_null()) {
20f8dbd7 1465 const auto& keyData = stringFromJson(document, privatekey_fieldname);
997cab68
BZ
1466 DNSKEYRecordContent dkrc;
1467 DNSSECPrivateKey dpk;
60b0a236 1468 try {
997cab68 1469 shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
a456662f
RG
1470 uint16_t flags = 0;
1471 if (keyOrZone) {
1472 flags = 257;
1473 }
1474 else {
1475 flags = 256;
1476 }
997cab68 1477
a456662f 1478 uint8_t algorithm = dkrc.d_algorithm;
a456662f
RG
1479 // TODO remove in 4.2.0
1480 if (algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1) {
c5153ca0 1481 algorithm = DNSSECKeeper::RSASHA1;
a456662f 1482 }
c5153ca0 1483 dpk.setKey(dke, flags, algorithm);
997cab68 1484 }
60b0a236
BZ
1485 catch (std::runtime_error& error) {
1486 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
212f57b8
FM
1487 }
1488 try {
7330f904 1489 if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, dpk, insertedId, active, published)) {
b727e19b
RG
1490 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1491 }
212f57b8
FM
1492 }
1493 catch (std::runtime_error& error) {
997cab68
BZ
1494 throw ApiException(error.what());
1495 }
20f8dbd7 1496 if (insertedId < 0) {
997cab68 1497 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
20f8dbd7 1498 }
212f57b8
FM
1499 }
1500 else {
5d9c6182 1501 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
997cab68 1502 }
7330f904 1503 apiZoneCryptokeysExport(zoneData.zoneName, insertedId, resp, &zoneData.dnssecKeeper);
997cab68 1504 resp->status = 201;
60b0a236 1505}
997cab68
BZ
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:
60b0a236 1511 * Case 1: invalid JSON data
997cab68 1512 * The server returns 400 Bad Request
60b0a236
BZ
1513 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1514 * The server returns 204 No Content
955cbfd0 1515 * Case 3: the backend returns false on de/activation. An error occurred.
997cab68
BZ
1516 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1517 * */
212f57b8
FM
1518static void apiZoneCryptokeysPUT(HttpRequest* req, HttpResponse* resp)
1519{
7330f904
FM
1520 ZoneData zoneData{req};
1521 const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
20f8dbd7
AT
1522
1523 if (inquireKeyId == -1) {
1524 throw HttpBadRequestException();
1525 }
212f57b8 1526 // throws an exception if the Body is empty
20f8dbd7 1527 const auto& document = req->json();
212f57b8 1528 // throws an exception if the key does not exist or is not a bool
997cab68 1529 bool active = boolFromJson(document, "active");
33918299 1530 bool published = boolFromJson(document, "published", true);
60b0a236 1531 if (active) {
7330f904
FM
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);
997cab68
BZ
1534 return;
1535 }
212f57b8
FM
1536 }
1537 else {
7330f904
FM
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);
997cab68
BZ
1540 return;
1541 }
1542 }
33918299
RG
1543
1544 if (published) {
7330f904
FM
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);
33918299
RG
1547 return;
1548 }
212f57b8
FM
1549 }
1550 else {
7330f904
FM
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);
33918299
RG
1553 return;
1554 }
1555 }
1556
60b0a236
BZ
1557 resp->body = "";
1558 resp->status = 204;
997cab68
BZ
1559}
1560
212f57b8
FM
1561static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, const DNSName& zonename)
1562{
ad528718 1563 DNSResourceRecord resourceRecord;
0f0e73fe 1564 vector<string> zonedata;
1f68b185 1565 stringtok(zonedata, zonestring, "\r\n");
0f0e73fe
MS
1566
1567 ZoneParserTNG zpt(zonedata, zonename);
ba3d53d1 1568 zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
e3fc3ebd 1569 zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
0f0e73fe 1570
212f57b8 1571 bool seenSOA = false;
0f0e73fe
MS
1572
1573 string comment = "Imported via the API";
1574
1575 try {
ad528718
FM
1576 while (zpt.get(resourceRecord, &comment)) {
1577 if (seenSOA && resourceRecord.qtype.getCode() == QType::SOA) {
0f0e73fe 1578 continue;
ad528718
FM
1579 }
1580 if (resourceRecord.qtype.getCode() == QType::SOA) {
212f57b8 1581 seenSOA = true;
ad528718
FM
1582 }
1583 validateGatheredRRType(resourceRecord);
0f0e73fe 1584
ad528718 1585 new_records.push_back(resourceRecord);
0f0e73fe
MS
1586 }
1587 }
212f57b8
FM
1588 catch (std::exception& ae) {
1589 throw ApiException("An error occurred while parsing the zonedata: " + string(ae.what()));
0f0e73fe
MS
1590 }
1591}
1592
ef2ea4bf 1593/** Throws ApiException if records which violate RRset constraints are present.
e3675a8a 1594 * NOTE: sorts records in-place.
646bcd7d
CH
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
e3675a8a 1600 */
2eb206ec
KM
1601static void checkNewRecords(vector<DNSResourceRecord>& records, const DNSName& zone)
1602{
e3675a8a 1603 sort(records.begin(), records.end(),
212f57b8
FM
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 });
646bcd7d 1608
e3675a8a 1609 DNSResourceRecord previous;
212f57b8 1610 for (const auto& rec : records) {
646bcd7d
CH
1611 if (previous.qname == rec.qname) {
1612 if (previous.qtype == rec.qtype) {
1613 if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
212f57b8 1614 throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " has more than one record");
646bcd7d
CH
1615 }
1616 if (previous.content == rec.content) {
d5fcd583 1617 throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " with content \"" + rec.content + "\"");
646bcd7d 1618 }
212f57b8
FM
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");
646bcd7d
CH
1622 }
1623 }
1624
2eb206ec
KM
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
646bcd7d
CH
1634 // Check if the DNSNames that should be hostnames, are hostnames
1635 try {
1636 checkHostnameCorrectness(rec);
212f57b8
FM
1637 }
1638 catch (const std::exception& e) {
1639 throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + ": " + e.what());
e3675a8a 1640 }
646bcd7d 1641
e3675a8a
CH
1642 previous = rec;
1643 }
1644}
1645
ad528718 1646static void checkTSIGKey(UeberBackend& backend, const DNSName& keyname, const DNSName& algo, const string& content)
212f57b8 1647{
6f16f97c
PL
1648 DNSName algoFromDB;
1649 string contentFromDB;
ad528718 1650 if (backend.getTSIGKey(keyname, algoFromDB, contentFromDB)) {
212f57b8 1651 throw HttpConflictException("A TSIG key with the name '" + keyname.toLogString() + "' already exists");
fa07a3cf
PL
1652 }
1653
ad528718 1654 TSIGHashEnum the{};
6f16f97c
PL
1655 if (!getTSIGHashEnum(algo, the)) {
1656 throw ApiException("Unknown TSIG algorithm: " + algo.toLogString());
1657 }
fa07a3cf 1658
6f16f97c
PL
1659 string b64out;
1660 if (B64Decode(content, b64out) == -1) {
1661 throw ApiException("TSIG content '" + content + "' cannot be base64-decoded");
fa07a3cf 1662 }
6f16f97c 1663}
fa07a3cf 1664
212f57b8
FM
1665static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content)
1666{
6f16f97c 1667 Json::object tsigkey = {
212f57b8
FM
1668 {"name", keyname.toStringNoDot()},
1669 {"id", apiZoneNameToId(keyname)},
1670 {"algorithm", algo.toStringNoDot()},
1671 {"key", content},
1672 {"type", "TSIGKey"}};
6f16f97c
PL
1673 return tsigkey;
1674}
fa07a3cf 1675
212f57b8
FM
1676static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent = true)
1677{
519a6288 1678 return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : "");
6f16f97c
PL
1679}
1680
212f57b8
FM
1681static void apiServerTSIGKeysGET(HttpRequest* /* req */, HttpResponse* resp)
1682{
efc47b04 1683 UeberBackend backend;
2775cadd 1684 vector<struct TSIGKey> keys;
6f16f97c 1685
efc47b04 1686 if (!backend.getTSIGKeys(keys)) {
2775cadd
AT
1687 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1688 }
6f16f97c 1689
2775cadd 1690 Json::array doc;
6f16f97c 1691
212f57b8 1692 for (const auto& key : keys) {
ad528718 1693 doc.emplace_back(makeJSONTSIGKey(key, false));
2775cadd
AT
1694 }
1695 resp->setJsonBody(doc);
1696}
6f16f97c 1697
212f57b8
FM
1698static void apiServerTSIGKeysPOST(HttpRequest* req, HttpResponse* resp)
1699{
efc47b04 1700 UeberBackend backend;
2775cadd
AT
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();
6f16f97c 1705
2775cadd
AT
1706 if (content.empty()) {
1707 try {
1708 content = makeTSIGKey(algo);
212f57b8
FM
1709 }
1710 catch (const PDNSException& exc) {
2775cadd 1711 throw HttpBadRequestException(exc.reason);
6f16f97c 1712 }
2775cadd 1713 }
6f16f97c 1714
2775cadd 1715 // Will throw an ApiException or HttpConflictException on error
efc47b04 1716 checkTSIGKey(backend, keyname, algo, content);
2775cadd 1717
efc47b04 1718 if (!backend.setTSIGKey(keyname, algo, content)) {
2775cadd 1719 throw HttpInternalServerErrorException("Unable to add TSIG key");
6f16f97c 1720 }
2775cadd
AT
1721
1722 resp->status = 201;
1723 resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content));
1724}
1725
6ab2f63a
FM
1726class TSIGKeyData
1727{
1728public:
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};
fa07a3cf 1752
212f57b8
FM
1753static void apiServerTSIGKeyDetailGET(HttpRequest* req, HttpResponse* resp)
1754{
6ab2f63a 1755 TSIGKeyData tsigKeyData{req};
fa07a3cf 1756
6ab2f63a 1757 resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey));
ca740e49 1758}
81c39bc6 1759
212f57b8
FM
1760static void apiServerTSIGKeyDetailPUT(HttpRequest* req, HttpResponse* resp)
1761{
6ab2f63a 1762 TSIGKeyData tsigKeyData{req};
81c39bc6 1763
ca740e49
AT
1764 const auto& document = req->json();
1765
1766 if (document["name"].is_string()) {
6ab2f63a 1767 tsigKeyData.tsigKey.name = DNSName(document["name"].string_value());
ca740e49
AT
1768 }
1769 if (document["algorithm"].is_string()) {
6ab2f63a 1770 tsigKeyData.tsigKey.algorithm = DNSName(document["algorithm"].string_value());
ca740e49 1771
ad528718 1772 TSIGHashEnum the{};
6ab2f63a
FM
1773 if (!getTSIGHashEnum(tsigKeyData.tsigKey.algorithm, the)) {
1774 throw ApiException("Unknown TSIG algorithm: " + tsigKeyData.tsigKey.algorithm.toLogString());
81c39bc6 1775 }
ca740e49
AT
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 + "'");
81c39bc6 1782 }
6ab2f63a 1783 tsigKeyData.tsigKey.key = std::move(new_content);
ca740e49 1784 }
6ab2f63a 1785 if (!tsigKeyData.backend.setTSIGKey(tsigKeyData.tsigKey.name, tsigKeyData.tsigKey.algorithm, tsigKeyData.tsigKey.key)) {
ca740e49
AT
1786 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1787 }
6ab2f63a 1788 if (tsigKeyData.tsigKey.name != tsigKeyData.keyName) {
ca740e49 1789 // Remove the old key
6ab2f63a
FM
1790 if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) {
1791 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'");
81c39bc6 1792 }
81c39bc6 1793 }
6ab2f63a 1794 resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey));
ca740e49
AT
1795}
1796
212f57b8
FM
1797static void apiServerTSIGKeyDetailDELETE(HttpRequest* req, HttpResponse* resp)
1798{
6ab2f63a
FM
1799 TSIGKeyData tsigKeyData{req};
1800 if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) {
1801 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'");
ca740e49
AT
1802 }
1803 resp->body = "";
1804 resp->status = 204;
1805}
1806
212f57b8
FM
1807static void apiServerAutoprimaryDetailDELETE(HttpRequest* req, HttpResponse* resp)
1808{
efc47b04 1809 UeberBackend backend;
61fb497a 1810 const AutoPrimary& primary{req->parameters["ip"], req->parameters["nameserver"], ""};
efc47b04 1811 if (!backend.autoPrimaryRemove(primary)) {
212f57b8 1812 throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
61fb497a
AT
1813 }
1814 resp->body = "";
1815 resp->status = 204;
1816}
1817
212f57b8
FM
1818static void apiServerAutoprimariesGET(HttpRequest* /* req */, HttpResponse* resp)
1819{
efc47b04 1820 UeberBackend backend;
782e9b24 1821
241dc035 1822 std::vector<AutoPrimary> primaries;
efc47b04 1823 if (!backend.autoPrimariesList(primaries)) {
241dc035
AT
1824 throw HttpInternalServerErrorException("Unable to retrieve autoprimaries");
1825 }
1826 Json::array doc;
212f57b8 1827 for (const auto& primary : primaries) {
241dc035 1828 const Json::object obj = {
212f57b8
FM
1829 {"ip", primary.ip},
1830 {"nameserver", primary.nameserver},
1831 {"account", primary.account}};
ad528718 1832 doc.emplace_back(obj);
241dc035
AT
1833 }
1834 resp->setJsonBody(doc);
1835}
782e9b24 1836
212f57b8
FM
1837static void apiServerAutoprimariesPOST(HttpRequest* req, HttpResponse* resp)
1838{
efc47b04 1839 UeberBackend backend;
782e9b24 1840
241dc035
AT
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();
782e9b24 1847 }
241dc035
AT
1848
1849 if (primary.ip.empty() or primary.nameserver.empty()) {
1850 throw ApiException("ip and nameserver fields must be filled");
1851 }
efc47b04 1852 if (!backend.autoPrimaryAdd(primary)) {
241dc035
AT
1853 throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
1854 }
1855 resp->body = "";
1856 resp->status = 201;
1857}
1858
b9ecbd3b 1859// create new zone
212f57b8
FM
1860static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp)
1861{
efc47b04
FM
1862 UeberBackend backend;
1863 DNSSECKeeper dnssecKeeper(&backend);
1864 DomainInfo domainInfo;
fb9be461 1865 const auto& document = req->json();
b9ecbd3b
CH
1866 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1867 apiCheckNameAllowedCharacters(zonename.toString());
1868 zonename.makeUsLowerCase();
4ebf78b1 1869
efc47b04 1870 bool exists = backend.getDomainInfo(zonename, domainInfo);
66ade8ac 1871 if (exists) {
b9ecbd3b 1872 throw HttpConflictException();
66ade8ac 1873 }
e2dba705 1874
8db3d7a6 1875 boost::optional<DomainInfo::DomainKind> kind;
d525b58b 1876 boost::optional<vector<ComboAddress>> primaries;
8db3d7a6
CH
1877 boost::optional<DNSName> catalog;
1878 boost::optional<string> account;
d525b58b 1879 extractDomainInfoFromDocument(document, kind, primaries, catalog, account);
8db3d7a6 1880
b9ecbd3b 1881 // validate 'kind' is set
8db3d7a6
CH
1882 if (!kind) {
1883 throw JsonException("Key 'kind' not present or not a String");
1884 }
1885 DomainInfo::DomainKind zonekind = *kind;
bb9fd223 1886
b9ecbd3b
CH
1887 string zonestring = document["zone"].string_value();
1888 auto rrsets = document["rrsets"];
66ade8ac 1889 if (rrsets.is_array() && !zonestring.empty()) {
b9ecbd3b 1890 throw ApiException("You cannot give rrsets AND zone data as text");
66ade8ac 1891 }
0f0e73fe 1892
21b4feba 1893 const auto& nameservers = document["nameservers"];
c02c999b 1894 if (!nameservers.is_null() && !nameservers.is_array() && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) {
b9ecbd3b 1895 throw ApiException("Nameservers is not a list");
66ade8ac 1896 }
e2dba705 1897
b9ecbd3b
CH
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;
0f0e73fe 1903
b9ecbd3b
CH
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) {
212f57b8 1912 throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
b9ecbd3b
CH
1913 }
1914 if (rrset["records"].is_array()) {
83c8688a 1915 uint32_t ttl = uintFromJson(rrset, "ttl");
b9ecbd3b
CH
1916 gatherRecords(rrset, qname, qtype, ttl, new_records);
1917 }
1918 if (rrset["comments"].is_array()) {
1919 gatherComments(rrset, qname, qtype, new_comments);
6754ef71
CH
1920 }
1921 }
212f57b8
FM
1922 }
1923 else if (!zonestring.empty()) {
b9ecbd3b 1924 gatherRecordsFromZone(zonestring, new_records, zonename);
cf17e6b8 1925 }
b9ecbd3b 1926 }
21b4feba
AT
1927 catch (const JsonException& exc) {
1928 throw ApiException("New RRsets are invalid: " + string(exc.what()));
b9ecbd3b 1929 }
0f0e73fe 1930
8db3d7a6
CH
1931 if (zonekind == DomainInfo::Consumer && !new_records.empty()) {
1932 throw ApiException("Zone data MUST NOT be given for Consumer zones");
1933 }
1934
efc47b04
FM
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");
66ade8ac
CH
1939 }
1940
efc47b04 1941 apiCheckQNameAllowedCharacters(resourceRecord.qname.toString());
f63168e6 1942
efc47b04 1943 if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) {
b9ecbd3b 1944 have_soa = true;
f63168e6 1945 }
efc47b04 1946 if (resourceRecord.qtype.getCode() == QType::NS && resourceRecord.qname == zonename) {
b9ecbd3b
CH
1947 have_zone_ns = true;
1948 }
1949 }
f7bfeb30 1950
b9ecbd3b
CH
1951 // synthesize RRs as needed
1952 DNSResourceRecord autorr;
1953 autorr.qname = zonename;
1954 autorr.auth = true;
1955 autorr.ttl = ::arg().asNum("default-ttl");
e2dba705 1956
c02c999b 1957 if (!have_soa && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) {
b9ecbd3b
CH
1958 // synthesize a SOA record so the zone "really" exists
1959 string soa = ::arg()["default-soa-content"];
1960 boost::replace_all(soa, "@", zonename.toStringNoDot());
efc47b04
FM
1961 SOAData soaData;
1962 fillSOAData(soa, soaData);
1963 soaData.serial = document["serial"].int_value();
b9ecbd3b 1964 autorr.qtype = QType::SOA;
efc47b04 1965 autorr.content = makeSOAContent(soaData)->getZoneRepresentation(true);
b9ecbd3b
CH
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();
66ade8ac 1973 if (nameserver.empty()) {
b9ecbd3b 1974 throw ApiException("Nameservers must be non-empty strings");
66ade8ac 1975 }
8db3d7a6
CH
1976 if (zonekind == DomainInfo::Consumer) {
1977 throw ApiException("Nameservers MUST NOT be given for Consumer zones");
1978 }
66ade8ac 1979 if (!isCanonical(nameserver)) {
b9ecbd3b 1980 throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
66ade8ac 1981 }
b9ecbd3b
CH
1982 try {
1983 // ensure the name parses
1984 autorr.content = DNSName(nameserver).toStringRootDot();
212f57b8
FM
1985 }
1986 catch (...) {
b9ecbd3b
CH
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");
e2dba705 1993 }
b9ecbd3b 1994 }
e2dba705 1995
b9ecbd3b 1996 checkNewRecords(new_records, zonename);
e3675a8a 1997
b9ecbd3b
CH
1998 if (boolFromJson(document, "dnssec", false)) {
1999 checkDefaultDNSSECAlgos();
986e4858 2000
212f57b8 2001 if (document["nsec3param"].string_value().length() > 0) {
b9ecbd3b 2002 NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
0e3e5d0b 2003 string error_msg;
efc47b04 2004 if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) {
212f57b8 2005 throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg);
986e4858
PL
2006 }
2007 }
b9ecbd3b 2008 }
986e4858 2009
b9ecbd3b 2010 // no going back after this
efc47b04 2011 if (!backend.createDomain(zonename, kind.get_value_or(DomainInfo::Native), primaries.get_value_or(vector<ComboAddress>()), account.get_value_or(""))) {
212f57b8 2012 throw ApiException("Creating domain '" + zonename.toString() + "' failed: backend refused");
66ade8ac 2013 }
f63168e6 2014
efc47b04 2015 if (!backend.getDomainInfo(zonename, domainInfo)) {
212f57b8 2016 throw ApiException("Creating domain '" + zonename.toString() + "' failed: lookup of domain ID failed");
66ade8ac 2017 }
f63168e6 2018
efc47b04 2019 domainInfo.backend->startTransaction(zonename, static_cast<int>(domainInfo.id));
f43646f5 2020
b9ecbd3b 2021 // will be overridden by updateDomainSettingsFromDocument, if given in document.
efc47b04 2022 domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", "DEFAULT");
9440a9f0 2023
efc47b04
FM
2024 for (auto& resourceRecord : new_records) {
2025 resourceRecord.domain_id = static_cast<int>(domainInfo.id);
2026 domainInfo.backend->feedRecord(resourceRecord, DNSName());
b9ecbd3b 2027 }
212f57b8 2028 for (Comment& comment : new_comments) {
efc47b04
FM
2029 comment.domain_id = static_cast<int>(domainInfo.id);
2030 if (!domainInfo.backend->feedComment(comment)) {
b9ecbd3b 2031 throw ApiException("Hosting backend does not support editing comments.");
f63168e6 2032 }
b9ecbd3b 2033 }
e2dba705 2034
efc47b04 2035 updateDomainSettingsFromDocument(backend, domainInfo, zonename, document, !new_records.empty());
f63168e6 2036
d525b58b 2037 if (!catalog && kind == DomainInfo::Primary) {
3b45a434 2038 const auto& defaultCatalog = ::arg()["default-catalog-zone"];
c8734ecd 2039 if (!defaultCatalog.empty()) {
efc47b04 2040 domainInfo.backend->setCatalog(zonename, DNSName(defaultCatalog));
c8734ecd
PD
2041 }
2042 }
2043
efc47b04 2044 domainInfo.backend->commitTransaction();
f4922e19 2045
efc47b04 2046 g_zoneCache.add(zonename, static_cast<int>(domainInfo.id)); // make new zone visible
e2dba705 2047
efc47b04 2048 fillZone(backend, zonename, resp, req);
b9ecbd3b
CH
2049 resp->status = 201;
2050}
c67bf8c5 2051
b9ecbd3b 2052// list known zones
212f57b8
FM
2053static void apiServerZonesGET(HttpRequest* req, HttpResponse* resp)
2054{
efc47b04
FM
2055 UeberBackend backend;
2056 DNSSECKeeper dnssecKeeper(&backend);
c67bf8c5 2057 vector<DomainInfo> domains;
e543cc8f 2058
ad528718 2059 if (req->getvars.count("zone") != 0) {
37e01df4 2060 string zone = req->getvars["zone"];
e543cc8f
CH
2061 apiCheckNameAllowedCharacters(zone);
2062 DNSName zonename = apiNameToDNSName(zone);
2063 zonename.makeUsLowerCase();
efc47b04
FM
2064 DomainInfo domainInfo;
2065 if (backend.getDomainInfo(zonename, domainInfo)) {
2066 domains.push_back(domainInfo);
e543cc8f 2067 }
212f57b8
FM
2068 }
2069 else {
72abd9ef 2070 try {
efc47b04 2071 backend.getAllDomains(&domains, true, true); // incl. serial and disabled
212f57b8 2072 }
efc47b04
FM
2073 catch (const PDNSException& exception) {
2074 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + exception.reason);
72abd9ef 2075 }
e543cc8f 2076 }
c67bf8c5 2077
07a32d18 2078 bool with_dnssec = true;
ad528718 2079 if (req->getvars.count("dnssec") != 0) {
07a32d18
CH
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
62a9a74c 2087 Json::array doc;
c8b929d9 2088 doc.reserve(domains.size());
ad528718 2089 for (const DomainInfo& domainInfo : domains) {
efc47b04 2090 doc.emplace_back(getZoneInfo(domainInfo, with_dnssec ? &dnssecKeeper : nullptr));
c67bf8c5 2091 }
917f686a 2092 resp->setJsonBody(doc);
c67bf8c5
CH
2093}
2094
212f57b8
FM
2095static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp)
2096{
7330f904 2097 ZoneData zoneData{req};
77bfe8de 2098
b62707f9
AT
2099 // update domain contents and/or settings
2100 const auto& document = req->json();
7c0ba3d2 2101
b62707f9
AT
2102 auto rrsets = document["rrsets"];
2103 bool zoneWasModified = false;
7330f904 2104 DomainInfo::DomainKind newKind = zoneData.domainInfo.kind;
b62707f9
AT
2105 if (document["kind"].is_string()) {
2106 newKind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
2107 }
26636b05 2108
b62707f9
AT
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;
7330f904
FM
2115 zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT-API", soaEditApiKind);
2116 zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT", soaEditKind);
70f1db7c 2117
b62707f9
AT
2118 vector<DNSResourceRecord> new_records;
2119 vector<Comment> new_comments;
70f1db7c 2120
b62707f9
AT
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) {
212f57b8 2128 throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
70f1db7c 2129 }
b62707f9
AT
2130 if (rrset["records"].is_array()) {
2131 uint32_t ttl = uintFromJson(rrset, "ttl");
2132 gatherRecords(rrset, qname, qtype, ttl, new_records);
66ade8ac 2133 }
b62707f9
AT
2134 if (rrset["comments"].is_array()) {
2135 gatherComments(rrset, qname, qtype, new_comments);
70f1db7c
CH
2136 }
2137 }
b62707f9
AT
2138 }
2139 catch (const JsonException& exc) {
2140 throw ApiException("New RRsets are invalid: " + string(exc.what()));
2141 }
70f1db7c 2142
efc47b04
FM
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");
8db3d7a6 2147 }
efc47b04 2148 apiCheckQNameAllowedCharacters(resourceRecord.qname.toString());
70f1db7c 2149
efc47b04 2150 if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zoneData.zoneName) {
b62707f9 2151 haveSoa = true;
70f1db7c 2152 }
b62707f9 2153 }
26636b05 2154
b62707f9
AT
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");
70f1db7c
CH
2162 }
2163
7330f904 2164 checkNewRecords(new_records, zoneData.zoneName);
7c0ba3d2 2165
7330f904 2166 zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id));
efc47b04
FM
2167 for (auto& resourceRecord : new_records) {
2168 resourceRecord.domain_id = static_cast<int>(zoneData.domainInfo.id);
2169 zoneData.domainInfo.backend->feedRecord(resourceRecord, DNSName());
b62707f9 2170 }
212f57b8 2171 for (Comment& comment : new_comments) {
7330f904
FM
2172 comment.domain_id = static_cast<int>(zoneData.domainInfo.id);
2173 zoneData.domainInfo.backend->feedComment(comment);
b62707f9 2174 }
db82ebde 2175
b62707f9 2176 if (!haveSoa && (newKind == DomainInfo::Secondary || newKind == DomainInfo::Consumer)) {
7330f904 2177 zoneData.domainInfo.backend->setStale(zoneData.domainInfo.id);
b62707f9 2178 }
212f57b8
FM
2179 }
2180 else {
b62707f9 2181 // avoid deleting current zone contents
7330f904 2182 zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1);
7c0ba3d2 2183 }
a44d979f 2184
b62707f9 2185 // updateDomainSettingsFromDocument will rectify the zone and update SOA serial.
7330f904
FM
2186 updateDomainSettingsFromDocument(zoneData.backend, zoneData.domainInfo, zoneData.zoneName, document, zoneWasModified);
2187 zoneData.domainInfo.backend->commitTransaction();
a44d979f 2188
7330f904 2189 purgeAuthCaches(zoneData.zoneName.toString() + "$");
b72b74c5 2190
b62707f9
AT
2191 resp->body = "";
2192 resp->status = 204; // No Content, but indicate success
2193}
2194
212f57b8
FM
2195static void apiServerZoneDetailDELETE(HttpRequest* req, HttpResponse* resp)
2196{
7330f904 2197 ZoneData zoneData{req};
b62707f9
AT
2198
2199 // delete domain
2200
7330f904 2201 zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1);
b62707f9 2202 try {
7330f904
FM
2203 if (!zoneData.domainInfo.backend->deleteDomain(zoneData.zoneName)) {
2204 throw ApiException("Deleting domain '" + zoneData.zoneName.toString() + "' failed: backend delete failed/unsupported");
a44d979f 2205 }
a462a01d 2206
7330f904 2207 zoneData.domainInfo.backend->commitTransaction();
5a4fa722 2208
7330f904 2209 g_zoneCache.remove(zoneData.zoneName);
212f57b8
FM
2210 }
2211 catch (...) {
7330f904 2212 zoneData.domainInfo.backend->abortTransaction();
b62707f9 2213 throw;
a462a01d 2214 }
b62707f9
AT
2215
2216 // clear caches
7330f904
FM
2217 DNSSECKeeper::clearCaches(zoneData.zoneName);
2218 purgeAuthCaches(zoneData.zoneName.toString() + "$");
b62707f9
AT
2219
2220 // empty body on success
2221 resp->body = "";
2222 resp->status = 204; // No Content: declare that the zone is gone now
2223}
2224
212f57b8
FM
2225static void apiServerZoneDetailPATCH(HttpRequest* req, HttpResponse* resp)
2226{
7330f904
FM
2227 ZoneData zoneData{req};
2228 patchZone(zoneData.backend, zoneData.zoneName, zoneData.domainInfo, req, resp);
b62707f9
AT
2229}
2230
212f57b8
FM
2231static void apiServerZoneDetailGET(HttpRequest* req, HttpResponse* resp)
2232{
7330f904
FM
2233 ZoneData zoneData{req};
2234 fillZone(zoneData.backend, zoneData.zoneName, resp, req);
b62707f9
AT
2235}
2236
212f57b8
FM
2237static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp)
2238{
7330f904 2239 ZoneData zoneData{req};
a83004d3 2240
ad528718 2241 ostringstream outputStringStream;
a83004d3 2242
ad528718
FM
2243 DNSResourceRecord resourceRecord;
2244 SOAData soaData;
7330f904
FM
2245 zoneData.domainInfo.backend->list(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id));
2246 while (zoneData.domainInfo.backend->get(resourceRecord)) {
ad528718 2247 if (resourceRecord.qtype.getCode() == 0) {
a83004d3 2248 continue; // skip empty non-terminals
ad528718 2249 }
a83004d3 2250
ad528718
FM
2251 outputStringStream << resourceRecord.qname.toString() << "\t" << resourceRecord.ttl << "\t"
2252 << "IN"
2253 << "\t" << resourceRecord.qtype.toString() << "\t" << makeApiRecordContent(resourceRecord.qtype, resourceRecord.content) << endl;
a83004d3
CH
2254 }
2255
2256 if (req->accept_json) {
ad528718 2257 resp->setJsonBody(Json::object{{"zone", outputStringStream.str()}});
212f57b8
FM
2258 }
2259 else {
a83004d3 2260 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
ad528718 2261 resp->body = outputStringStream.str();
a83004d3
CH
2262 }
2263}
2264
212f57b8
FM
2265static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp)
2266{
7330f904 2267 ZoneData zoneData{req};
a426cb89 2268
7330f904
FM
2269 if (zoneData.domainInfo.primaries.empty()) {
2270 throw ApiException("Domain '" + zoneData.zoneName.toString() + "' is not a secondary domain (or has no primary defined)");
ad528718 2271 }
a426cb89 2272
7330f904
FM
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());
a426cb89
CH
2276}
2277
212f57b8
FM
2278static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp)
2279{
7330f904 2280 ZoneData zoneData{req};
a426cb89 2281
7330f904 2282 if (!Communicator.notifyDomain(zoneData.zoneName, &zoneData.backend)) {
a426cb89 2283 throw ApiException("Failed to add to the queue - see server log");
ad528718 2284 }
a426cb89 2285
692829aa 2286 resp->setSuccessResult("Notification queued");
a426cb89
CH
2287}
2288
212f57b8
FM
2289static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp)
2290{
7330f904 2291 ZoneData zoneData{req};
4bc8379e 2292
7330f904
FM
2293 if (zoneData.dnssecKeeper.isPresigned(zoneData.zoneName)) {
2294 throw ApiException("Zone '" + zoneData.zoneName.toString() + "' is pre-signed, not rectifying.");
ad528718 2295 }
4bc8379e 2296
ad528718 2297 string error_msg;
59102608 2298 string info;
7330f904
FM
2299 if (!zoneData.dnssecKeeper.rectifyZone(zoneData.zoneName, error_msg, info, true)) {
2300 throw ApiException("Failed to rectify '" + zoneData.zoneName.toString() + "' " + error_msg);
ad528718 2301 }
4bc8379e
PL
2302
2303 resp->setSuccessResult("Rectified");
2304}
2305
efc47b04
FM
2306// NOLINTNEXTLINE(readability-function-cognitive-complexity): TODO Refactor this function.
2307static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp)
994a94ce 2308{
ad528718
FM
2309 bool zone_disabled = false;
2310 SOAData soaData;
b3905a3d 2311
f63168e6
CH
2312 vector<DNSResourceRecord> new_records;
2313 vector<Comment> new_comments;
d708640f
CH
2314 vector<DNSResourceRecord> new_ptrs;
2315
1f68b185 2316 Json document = req->json();
b3905a3d 2317
1f68b185 2318 auto rrsets = document["rrsets"];
ad528718 2319 if (!rrsets.is_array()) {
d708640f 2320 throw ApiException("No rrsets given in update request");
ad528718 2321 }
b3905a3d 2322
efc47b04 2323 domainInfo.backend->startTransaction(zonename);
6cc98ddf 2324
d708640f 2325 try {
d29d5db7 2326 string soa_edit_api_kind;
a6448d95 2327 string soa_edit_kind;
efc47b04
FM
2328 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
2329 domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
d29d5db7
CH
2330 bool soa_edit_done = false;
2331
905dae56 2332 set<std::tuple<DNSName, QType, string>> seen;
2b048cc7 2333
6754ef71
CH
2334 for (const auto& rrset : rrsets.array_items()) {
2335 string changetype = toUpper(stringFromJson(rrset, "changetype"));
c576d0c5 2336 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
cb9b5901 2337 apiCheckQNameAllowedCharacters(qname.toString());
6754ef71 2338 QType qtype;
d708640f 2339 qtype = stringFromJson(rrset, "type");
6754ef71 2340 if (qtype.getCode() == 0) {
212f57b8 2341 throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
6754ef71 2342 }
d708640f 2343
ad528718 2344 if (seen.count({qname, qtype, changetype}) != 0) {
212f57b8 2345 throw ApiException("Duplicate RRset " + qname.toString() + " IN " + qtype.toString() + " with changetype: " + changetype);
2b048cc7 2346 }
e732e9cc 2347 seen.insert({qname, qtype, changetype});
d708640f 2348
d708640f 2349 if (changetype == "DELETE") {
b7f21ab1 2350 // delete all matching qname/qtype RRs (and, implicitly comments).
efc47b04 2351 if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, vector<DNSResourceRecord>())) {
d708640f 2352 throw ApiException("Hosting backend does not support editing records.");
6cc98ddf 2353 }
d708640f
CH
2354 }
2355 else if (changetype == "REPLACE") {
1d6b70f9 2356 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
ad528718 2357 if (!qname.isPartOf(zonename) && qname != zonename) {
212f57b8 2358 throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Name is out of zone");
ad528718 2359 }
34df6ecc 2360
6754ef71
CH
2361 bool replace_records = rrset["records"].is_array();
2362 bool replace_comments = rrset["comments"].is_array();
f63168e6 2363
6754ef71 2364 if (!replace_records && !replace_comments) {
d5fcd583 2365 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.toString());
6754ef71 2366 }
f63168e6 2367
6754ef71
CH
2368 new_records.clear();
2369 new_comments.clear();
f63168e6 2370
cf17e6b8
CH
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.
83c8688a 2374 uint32_t ttl = uintFromJson(rrset, "ttl");
cf17e6b8
CH
2375 gatherRecords(rrset, qname, qtype, ttl, new_records);
2376
ad528718 2377 for (DNSResourceRecord& resourceRecord : new_records) {
efc47b04 2378 resourceRecord.domain_id = static_cast<int>(domainInfo.id);
ad528718
FM
2379 if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) {
2380 soa_edit_done = increaseSOARecord(resourceRecord, soa_edit_api_kind, soa_edit_kind);
cf17e6b8 2381 }
6754ef71 2382 }
cf17e6b8 2383 checkNewRecords(new_records, zonename);
d708640f 2384 }
6cc98ddf 2385
cf17e6b8
CH
2386 if (replace_comments) {
2387 gatherComments(rrset, qname, qtype, new_comments);
f63168e6 2388
ad528718 2389 for (Comment& comment : new_comments) {
efc47b04 2390 comment.domain_id = static_cast<int>(domainInfo.id);
cf17e6b8 2391 }
6754ef71 2392 }
d708640f 2393 }
cf17e6b8
CH
2394 catch (const JsonException& e) {
2395 throw ApiException("New RRsets are invalid: " + string(e.what()));
2396 }
b3905a3d 2397
d708640f 2398 if (replace_records) {
03b1cc25 2399 bool ent_present = false;
ad528718
FM
2400 bool dname_seen = false;
2401 bool ns_seen = false;
5e73d266 2402
efc47b04 2403 domainInfo.backend->lookup(QType(QType::ANY), qname, static_cast<int>(domainInfo.id));
ad528718 2404 DNSResourceRecord resourceRecord;
efc47b04 2405 while (domainInfo.backend->get(resourceRecord)) {
ad528718 2406 if (resourceRecord.qtype.getCode() == QType::ENT) {
03b1cc25 2407 ent_present = true;
002c4fe1
RG
2408 /* that's fine, we will override it */
2409 continue;
03b1cc25 2410 }
ad528718 2411 if (qtype == QType::DNAME || resourceRecord.qtype == QType::DNAME) {
5e73d266 2412 dname_seen = true;
ad528718
FM
2413 }
2414 if (qtype == QType::NS || resourceRecord.qtype == QType::NS) {
5e73d266 2415 ns_seen = true;
ad528718
FM
2416 }
2417 if (qtype.getCode() != resourceRecord.qtype.getCode()
212f57b8 2418 && (exclusiveEntryTypes.count(qtype.getCode()) != 0
ad528718 2419 || exclusiveEntryTypes.count(resourceRecord.qtype.getCode()) != 0)) {
e4e8ca30
PD
2420
2421 // leave database handle in a consistent state
efc47b04 2422 while (domainInfo.backend->get(resourceRecord)) {
e4e8ca30 2423 ;
ad528718 2424 }
e4e8ca30 2425
212f57b8 2426 throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Conflicts with pre-existing RRset");
8560f36a
CH
2427 }
2428 }
2429
5e73d266 2430 if (dname_seen && ns_seen && qname != zonename) {
212f57b8 2431 throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Cannot have both NS and DNAME except in zone apex");
5e73d266 2432 }
efc47b04 2433 if (!new_records.empty() && domainInfo.kind == DomainInfo::Consumer) {
8db3d7a6
CH
2434 // Allow deleting all RRsets, just not modifying them.
2435 throw ApiException("Modifying RRsets in Consumer zones is unsupported");
2436 }
03b1cc25
CH
2437 if (!new_records.empty() && ent_present) {
2438 QType qt_ent{0};
efc47b04 2439 if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qt_ent, new_records)) {
03b1cc25
CH
2440 throw ApiException("Hosting backend does not support editing records.");
2441 }
2442 }
efc47b04 2443 if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, new_records)) {
d708640f
CH
2444 throw ApiException("Hosting backend does not support editing records.");
2445 }
2446 }
2447 if (replace_comments) {
efc47b04 2448 if (!domainInfo.backend->replaceComments(domainInfo.id, qname, qtype, new_comments)) {
d708640f
CH
2449 throw ApiException("Hosting backend does not support editing comments.");
2450 }
2451 }
6cc98ddf 2452 }
ad528718 2453 else {
d708640f 2454 throw ApiException("Changetype not understood");
ad528718 2455 }
6cc98ddf 2456 }
d29d5db7 2457
efc47b04 2458 zone_disabled = (!backend.getSOAUncached(zonename, soaData));
d29d5db7 2459
a41a789a
KM
2460 // edit SOA (if needed)
2461 if (!zone_disabled && !soa_edit_api_kind.empty() && !soa_edit_done) {
ad528718
FM
2462 DNSResourceRecord resourceRecord;
2463 if (makeIncreasedSOARecord(soaData, soa_edit_api_kind, soa_edit_kind, resourceRecord)) {
efc47b04 2464 if (!domainInfo.backend->replaceRRSet(domainInfo.id, resourceRecord.qname, resourceRecord.qtype, vector<DNSResourceRecord>(1, resourceRecord))) {
13f9e280
CH
2465 throw ApiException("Hosting backend does not support editing records.");
2466 }
d29d5db7 2467 }
3ae63ca8 2468
478de03b 2469 // return old and new serials in headers
ad528718
FM
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);
d29d5db7 2473 }
212f57b8
FM
2474 }
2475 catch (...) {
efc47b04 2476 domainInfo.backend->abortTransaction();
d708640f
CH
2477 throw;
2478 }
986e4858 2479
0ca59ad9 2480 // Rectify
efc47b04
FM
2481 DNSSECKeeper dnssecKeeper(&backend);
2482 if (!zone_disabled && !dnssecKeeper.isPresigned(zonename) && isZoneApiRectifyEnabled(domainInfo)) {
d71d2c88
CH
2483 string info;
2484 string error_msg;
ad528718 2485 if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false)) {
d71d2c88 2486 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
0ca59ad9 2487 }
986e4858
PL
2488 }
2489
efc47b04 2490 domainInfo.backend->commitTransaction();
b3905a3d 2491
cbc5c674 2492 DNSSECKeeper::clearCaches(zonename);
27e14380 2493 purgeAuthCaches(zonename.toString() + "$");
d1587ceb 2494
f0e76cee
CH
2495 resp->body = "";
2496 resp->status = 204; // No Content, but indicate success
b3905a3d
CH
2497}
2498
212f57b8
FM
2499static void apiServerSearchData(HttpRequest* req, HttpResponse* resp)
2500{
ad528718
FM
2501 string qVar = req->getvars["q"];
2502 string sMaxVar = req->getvars["max"];
2503 string sObjectTypeVar = req->getvars["object_type"];
45250285 2504
fd9af6ec
FM
2505 size_t maxEnts = 100;
2506 size_t ents = 0;
720ed2bd 2507
45250285 2508 // the following types of data can be searched for using the api
0bd91de6 2509 enum class ObjectType
45250285
JE
2510 {
2511 ALL,
2512 ZONE,
2513 RECORD,
2514 COMMENT
ad528718 2515 } objectType{};
45250285 2516
ad528718 2517 if (qVar.empty()) {
b1902fab 2518 throw ApiException("Query q can't be blank");
ad528718
FM
2519 }
2520 if (!sMaxVar.empty()) {
2521 maxEnts = std::stoi(sMaxVar);
2522 }
2523 if (maxEnts < 1) {
720ed2bd 2524 throw ApiException("Maximum entries must be larger than 0");
ad528718 2525 }
b1902fab 2526
ad528718 2527 if (sObjectTypeVar.empty() || sObjectTypeVar == "all") {
0bd91de6 2528 objectType = ObjectType::ALL;
ad528718
FM
2529 }
2530 else if (sObjectTypeVar == "zone") {
0bd91de6 2531 objectType = ObjectType::ZONE;
ad528718
FM
2532 }
2533 else if (sObjectTypeVar == "record") {
0bd91de6 2534 objectType = ObjectType::RECORD;
ad528718
FM
2535 }
2536 else if (sObjectTypeVar == "comment") {
0bd91de6 2537 objectType = ObjectType::COMMENT;
ad528718
FM
2538 }
2539 else {
0bd91de6 2540 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
ad528718 2541 }
45250285 2542
ad528718
FM
2543 SimpleMatch simpleMatch(qVar, true);
2544 UeberBackend backend;
b1902fab 2545 vector<DomainInfo> domains;
720ed2bd
AT
2546 vector<DNSResourceRecord> result_rr;
2547 vector<Comment> result_c;
212f57b8
FM
2548 map<int, DomainInfo> zoneIdZone;
2549 map<int, DomainInfo>::iterator val;
00963dea 2550 Json::array doc;
b1902fab 2551
ad528718 2552 backend.getAllDomains(&domains, false, true);
d2d194a9 2553
ad528718
FM
2554 for (const DomainInfo& domainInfo : domains) {
2555 if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && simpleMatch.match(domainInfo.zone)) {
212f57b8
FM
2556 doc.push_back(Json::object{
2557 {"object_type", "zone"},
ad528718
FM
2558 {"zone_id", apiZoneNameToId(domainInfo.zone)},
2559 {"name", domainInfo.zone.toString()}});
720ed2bd 2560 ents++;
b1902fab 2561 }
ad528718 2562 zoneIdZone[static_cast<int>(domainInfo.id)] = domainInfo; // populate cache
720ed2bd 2563 }
b1902fab 2564
ad528718
FM
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) {
7cbc5255 2568 continue; // skip empty non-terminals
ad528718 2569 }
7cbc5255 2570
212f57b8
FM
2571 auto object = Json::object{
2572 {"object_type", "record"},
ad528718
FM
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()) {
00963dea
CH
2581 object["zone_id"] = apiZoneNameToId(val->second.zone);
2582 object["zone"] = val->second.zone.toString();
720ed2bd 2583 }
ad528718 2584 doc.emplace_back(object);
b1902fab 2585 }
720ed2bd 2586 }
b1902fab 2587
ad528718
FM
2588 if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && backend.searchComments(qVar, maxEnts, result_c)) {
2589 for (const Comment& comment : result_c) {
212f57b8
FM
2590 auto object = Json::object{
2591 {"object_type", "comment"},
ad528718
FM
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()) {
00963dea
CH
2598 object["zone_id"] = apiZoneNameToId(val->second.zone);
2599 object["zone"] = val->second.zone.toString();
720ed2bd 2600 }
ad528718 2601 doc.emplace_back(object);
b1902fab
CH
2602 }
2603 }
4bd3d119 2604
917f686a 2605 resp->setJsonBody(doc);
b1902fab
CH
2606}
2607
212f57b8
FM
2608static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp)
2609{
c0f6a1da
CH
2610 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
2611
37b89580 2612 if (g_zoneCache.isEnabled()) {
ad528718
FM
2613 DomainInfo domainInfo;
2614 UeberBackend backend;
2615 if (backend.getDomainInfo(canon, domainInfo, false)) {
37b89580
CH
2616 // zone exists (uncached), add/update it in the zone cache.
2617 // Handle this first, to avoid concurrent queries re-populating the other caches.
ad528718 2618 g_zoneCache.add(domainInfo.zone, static_cast<int>(domainInfo.id));
37b89580 2619 }
b72b74c5 2620 else {
ad528718 2621 g_zoneCache.remove(domainInfo.zone);
b72b74c5 2622 }
37b89580
CH
2623 }
2624
cbc5c674 2625 DNSSECKeeper::clearCaches(canon);
7bae5ce4
CH
2626 // purge entire zone from cache, not just zone-level records.
2627 uint64_t count = purgeAuthCaches(canon.toString() + "$");
212f57b8
FM
2628 resp->setJsonBody(Json::object{
2629 {"count", (int)count},
2630 {"result", "Flushed cache."}});
ddc84d12
CH
2631}
2632
ad528718 2633static std::ostream& operator<<(std::ostream& outStream, StatType statType)
dec69610 2634{
212f57b8
FM
2635 switch (statType) {
2636 case StatType::counter:
ad528718 2637 return outStream << "counter";
212f57b8 2638 case StatType::gauge:
ad528718 2639 return outStream << "gauge";
dec69610 2640 };
ad528718 2641 return outStream << static_cast<uint16_t>(statType);
dec69610
MTH
2642}
2643
212f57b8
FM
2644static void prometheusMetrics(HttpRequest* /* req */, HttpResponse* resp)
2645{
dec69610 2646 std::ostringstream output;
212f57b8 2647 for (const auto& metricName : S.getEntries()) {
dec69610
MTH
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
212f57b8
FM
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";
df04f36b 2665
dec69610
MTH
2666 resp->body = output.str();
2667 resp->headers["Content-Type"] = "text/plain";
2668 resp->status = 200;
2669}
2670
ad528718 2671static void cssfunction(HttpRequest* /* req */, HttpResponse* resp)
c67bf8c5 2672{
80d59cd1
CH
2673 resp->headers["Cache-Control"] = "max-age=86400";
2674 resp->headers["Content-Type"] = "text/css";
c67bf8c5 2675
1071abdd 2676 ostringstream ret;
212f57b8
FM
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;
80d59cd1 2703 resp->body = ret.str();
c146576d 2704 resp->status = 200;
1071abdd
CH
2705}
2706
dea47634 2707void AuthWebServer::webThread()
12c86877
BH
2708{
2709 try {
519f5484 2710 setThreadName("pdns/webserver");
212f57b8 2711 if (::arg().mustDo("api")) {
a9c69ea5
AT
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");
c67bf8c5 2750 }
536ab56f 2751 if (::arg().mustDo("webserver")) {
212f57b8 2752 d_ws->registerWebHandler(
ad528718 2753 "/style.css", [](HttpRequest* req, HttpResponse* resp) { cssfunction(req, resp); }, "GET");
212f57b8
FM
2754 d_ws->registerWebHandler(
2755 "/", [this](HttpRequest* req, HttpResponse* resp) { indexfunction(req, resp); }, "GET");
a9c69ea5 2756 d_ws->registerWebHandler("/metrics", prometheusMetrics, "GET");
536ab56f 2757 }
96d299db 2758 d_ws->go();
12c86877 2759 }
212f57b8
FM
2760 catch (...) {
2761 g_log << Logger::Error << "AuthWebServer thread caught an exception, dying" << endl;
5bd2ea7b 2762 _exit(1);
12c86877
BH
2763 }
2764}