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