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