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