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