]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/ws-auth.cc
Merge pull request #7787 from rgacogne/auth-api-replace-ent
[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) {
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, true)) {
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 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1646 if (!soa_edit_api_kind.empty()) {
1647 di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1648 }
1649
1650 di.backend->startTransaction(zonename, di.id);
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);
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 updateDomainSettingsFromDocument(B, di, zonename, req->json());
1718
1719 resp->body = "";
1720 resp->status = 204; // No Content, but indicate success
1721 return;
1722 }
1723 else if(req->method == "DELETE") {
1724 // delete domain
1725 if(!di.backend->deleteDomain(zonename))
1726 throw ApiException("Deleting domain '"+zonename.toString()+"' failed: backend delete failed/unsupported");
1727
1728 // clear caches
1729 DNSSECKeeper dk(&B);
1730 dk.clearCaches(zonename);
1731 purgeAuthCaches(zonename.toString() + "$");
1732
1733 // empty body on success
1734 resp->body = "";
1735 resp->status = 204; // No Content: declare that the zone is gone now
1736 return;
1737 } else if (req->method == "PATCH") {
1738 patchZone(req, resp);
1739 return;
1740 } else if (req->method == "GET") {
1741 fillZone(zonename, resp, shouldDoRRSets(req));
1742 return;
1743 }
1744 throw HttpMethodNotAllowedException();
1745 }
1746
1747 static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
1748 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1749
1750 if(req->method != "GET")
1751 throw HttpMethodNotAllowedException();
1752
1753 ostringstream ss;
1754
1755 UeberBackend B;
1756 DomainInfo di;
1757 if (!B.getDomainInfo(zonename, di)) {
1758 throw HttpNotFoundException();
1759 }
1760
1761 DNSResourceRecord rr;
1762 SOAData sd;
1763 di.backend->list(zonename, di.id);
1764 while(di.backend->get(rr)) {
1765 if (!rr.qtype.getCode())
1766 continue; // skip empty non-terminals
1767
1768 ss <<
1769 rr.qname.toString() << "\t" <<
1770 rr.ttl << "\t" <<
1771 "IN" << "\t" <<
1772 rr.qtype.getName() << "\t" <<
1773 makeApiRecordContent(rr.qtype, rr.content) <<
1774 endl;
1775 }
1776
1777 if (req->accept_json) {
1778 resp->setBody(Json::object { { "zone", ss.str() } });
1779 } else {
1780 resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
1781 resp->body = ss.str();
1782 }
1783 }
1784
1785 static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
1786 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1787
1788 if(req->method != "PUT")
1789 throw HttpMethodNotAllowedException();
1790
1791 UeberBackend B;
1792 DomainInfo di;
1793 if (!B.getDomainInfo(zonename, di)) {
1794 throw HttpNotFoundException();
1795 }
1796
1797 if(di.masters.empty())
1798 throw ApiException("Domain '"+zonename.toString()+"' is not a slave domain (or has no master defined)");
1799
1800 random_shuffle(di.masters.begin(), di.masters.end());
1801 Communicator.addSuckRequest(zonename, di.masters.front());
1802 resp->setSuccessResult("Added retrieval request for '"+zonename.toString()+"' from master "+di.masters.front().toLogString());
1803 }
1804
1805 static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
1806 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1807
1808 if(req->method != "PUT")
1809 throw HttpMethodNotAllowedException();
1810
1811 UeberBackend B;
1812 DomainInfo di;
1813 if (!B.getDomainInfo(zonename, di)) {
1814 throw HttpNotFoundException();
1815 }
1816
1817 if(!Communicator.notifyDomain(zonename))
1818 throw ApiException("Failed to add to the queue - see server log");
1819
1820 resp->setSuccessResult("Notification queued");
1821 }
1822
1823 static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp) {
1824 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1825
1826 if(req->method != "PUT")
1827 throw HttpMethodNotAllowedException();
1828
1829 UeberBackend B;
1830 DomainInfo di;
1831 if (!B.getDomainInfo(zonename, di)) {
1832 throw HttpNotFoundException();
1833 }
1834
1835 DNSSECKeeper dk(&B);
1836
1837 if (!dk.isSecuredZone(zonename))
1838 throw ApiException("Zone '" + zonename.toString() + "' is not DNSSEC signed, not rectifying.");
1839
1840 if (di.kind == DomainInfo::Slave)
1841 throw ApiException("Zone '" + zonename.toString() + "' is a slave zone, not rectifying.");
1842
1843 string error_msg = "";
1844 string info;
1845 if (!dk.rectifyZone(zonename, error_msg, info, true))
1846 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
1847
1848 resp->setSuccessResult("Rectified");
1849 }
1850
1851 static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
1852 if (rr.qtype.getCode() == QType::A) {
1853 uint32_t ip;
1854 if (!IpToU32(rr.content, &ip)) {
1855 throw ApiException("PTR: Invalid IP address given");
1856 }
1857 ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1858 % ((ip >> 24) & 0xff)
1859 % ((ip >> 16) & 0xff)
1860 % ((ip >> 8) & 0xff)
1861 % ((ip ) & 0xff)
1862 ).str());
1863 } else if (rr.qtype.getCode() == QType::AAAA) {
1864 ComboAddress ca(rr.content);
1865 char buf[3];
1866 ostringstream ss;
1867 for (int octet = 0; octet < 16; ++octet) {
1868 if (snprintf(buf, sizeof(buf), "%02x", ca.sin6.sin6_addr.s6_addr[octet]) != (sizeof(buf)-1)) {
1869 // this should be impossible: no byte should give more than two digits in hex format
1870 throw PDNSException("Formatting IPv6 address failed");
1871 }
1872 ss << buf[0] << '.' << buf[1] << '.';
1873 }
1874 string tmp = ss.str();
1875 tmp.resize(tmp.size()-1); // remove last dot
1876 // reverse and append arpa domain
1877 ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
1878 } else {
1879 throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
1880 }
1881
1882 ptr->qtype = "PTR";
1883 ptr->ttl = rr.ttl;
1884 ptr->disabled = rr.disabled;
1885 ptr->content = rr.qname.toStringRootDot();
1886 }
1887
1888 static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs) {
1889 for(const DNSResourceRecord& rr : new_ptrs) {
1890 SOAData sd;
1891 if (!B.getAuth(rr.qname, QType(QType::PTR), &sd, false))
1892 throw ApiException("Could not find domain for PTR '"+rr.qname.toString()+"' requested for '"+rr.content+"' (while saving)");
1893
1894 string soa_edit_api_kind;
1895 string soa_edit_kind;
1896 bool soa_changed = false;
1897 DNSResourceRecord soarr;
1898 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT-API", soa_edit_api_kind);
1899 sd.db->getDomainMetadataOne(sd.qname, "SOA-EDIT", soa_edit_kind);
1900 if (!soa_edit_api_kind.empty()) {
1901 soa_changed = makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, soarr);
1902 }
1903
1904 sd.db->startTransaction(sd.qname);
1905 if (!sd.db->replaceRRSet(sd.domain_id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
1906 sd.db->abortTransaction();
1907 throw ApiException("PTR-Hosting backend for "+rr.qname.toString()+"/"+rr.qtype.getName()+" does not support editing records.");
1908 }
1909
1910 if (soa_changed) {
1911 sd.db->replaceRRSet(sd.domain_id, soarr.qname, soarr.qtype, vector<DNSResourceRecord>(1, soarr));
1912 }
1913
1914 sd.db->commitTransaction();
1915 purgeAuthCachesExact(rr.qname);
1916 }
1917 }
1918
1919 static void patchZone(HttpRequest* req, HttpResponse* resp) {
1920 UeberBackend B;
1921 DomainInfo di;
1922 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
1923 if (!B.getDomainInfo(zonename, di)) {
1924 throw HttpNotFoundException();
1925 }
1926
1927 vector<DNSResourceRecord> new_records;
1928 vector<Comment> new_comments;
1929 vector<DNSResourceRecord> new_ptrs;
1930
1931 Json document = req->json();
1932
1933 auto rrsets = document["rrsets"];
1934 if (!rrsets.is_array())
1935 throw ApiException("No rrsets given in update request");
1936
1937 di.backend->startTransaction(zonename);
1938
1939 try {
1940 string soa_edit_api_kind;
1941 string soa_edit_kind;
1942 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
1943 di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
1944 bool soa_edit_done = false;
1945
1946 set<pair<DNSName, QType>> seen;
1947
1948 for (const auto& rrset : rrsets.array_items()) {
1949 string changetype = toUpper(stringFromJson(rrset, "changetype"));
1950 DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
1951 apiCheckQNameAllowedCharacters(qname.toString());
1952 QType qtype;
1953 qtype = stringFromJson(rrset, "type");
1954 if (qtype.getCode() == 0) {
1955 throw ApiException("RRset "+qname.toString()+" IN "+stringFromJson(rrset, "type")+": unknown type given");
1956 }
1957
1958 if(seen.count({qname, qtype}))
1959 {
1960 throw ApiException("Duplicate RRset "+qname.toString()+" IN "+qtype.getName());
1961 }
1962 seen.insert({qname, qtype});
1963
1964 if (changetype == "DELETE") {
1965 // delete all matching qname/qtype RRs (and, implicitly comments).
1966 if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
1967 throw ApiException("Hosting backend does not support editing records.");
1968 }
1969 }
1970 else if (changetype == "REPLACE") {
1971 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1972 if (!qname.isPartOf(zonename) && qname != zonename)
1973 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
1974
1975 bool replace_records = rrset["records"].is_array();
1976 bool replace_comments = rrset["comments"].is_array();
1977
1978 if (!replace_records && !replace_comments) {
1979 throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.getName());
1980 }
1981
1982 new_records.clear();
1983 new_comments.clear();
1984
1985 if (replace_records) {
1986 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1987 int ttl = intFromJson(rrset, "ttl");
1988 // new_ptrs is merged.
1989 gatherRecords(req->logprefix, rrset, qname, qtype, ttl, new_records, new_ptrs);
1990
1991 for(DNSResourceRecord& rr : new_records) {
1992 rr.domain_id = di.id;
1993 if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
1994 soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
1995 }
1996 }
1997 checkNewRecords(new_records);
1998 }
1999
2000 if (replace_comments) {
2001 gatherComments(rrset, qname, qtype, new_comments);
2002
2003 for(Comment& c : new_comments) {
2004 c.domain_id = di.id;
2005 }
2006 }
2007
2008 if (replace_records) {
2009 bool ent_present = false;
2010 di.backend->lookup(QType(QType::ANY), qname);
2011 DNSResourceRecord rr;
2012 while (di.backend->get(rr)) {
2013 if (rr.qtype.getCode() == 0) {
2014 ent_present = true;
2015 /* that's fine, we will override it */
2016 continue;
2017 }
2018 if (qtype.getCode() != rr.qtype.getCode()
2019 && (exclusiveEntryTypes.count(qtype.getCode()) != 0
2020 || exclusiveEntryTypes.count(rr.qtype.getCode()) != 0)) {
2021 throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Conflicts with pre-existing RRset");
2022 }
2023 }
2024
2025 if (!new_records.empty() && ent_present) {
2026 QType qt_ent{0};
2027 if (!di.backend->replaceRRSet(di.id, qname, qt_ent, new_records)) {
2028 throw ApiException("Hosting backend does not support editing records.");
2029 }
2030 }
2031 if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) {
2032 throw ApiException("Hosting backend does not support editing records.");
2033 }
2034 }
2035 if (replace_comments) {
2036 if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) {
2037 throw ApiException("Hosting backend does not support editing comments.");
2038 }
2039 }
2040 }
2041 else
2042 throw ApiException("Changetype not understood");
2043 }
2044
2045 // edit SOA (if needed)
2046 if (!soa_edit_api_kind.empty() && !soa_edit_done) {
2047 SOAData sd;
2048 if (!B.getSOAUncached(zonename, sd))
2049 throw ApiException("No SOA found for domain '"+zonename.toString()+"'");
2050
2051 DNSResourceRecord rr;
2052 if (makeIncreasedSOARecord(sd, soa_edit_api_kind, soa_edit_kind, rr)) {
2053 if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
2054 throw ApiException("Hosting backend does not support editing records.");
2055 }
2056 }
2057
2058 // return old and new serials in headers
2059 resp->headers["X-PDNS-Old-Serial"] = std::to_string(sd.serial);
2060 fillSOAData(rr.content, sd);
2061 resp->headers["X-PDNS-New-Serial"] = std::to_string(sd.serial);
2062 }
2063
2064 } catch(...) {
2065 di.backend->abortTransaction();
2066 throw;
2067 }
2068
2069 DNSSECKeeper dk(&B);
2070 string api_rectify;
2071 di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
2072 if (dk.isSecuredZone(zonename) && !dk.isPresigned(zonename) && api_rectify == "1") {
2073 string error_msg = "";
2074 string info;
2075 if (!dk.rectifyZone(zonename, error_msg, info, false))
2076 throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
2077 }
2078
2079 di.backend->commitTransaction();
2080
2081 purgeAuthCachesExact(zonename);
2082
2083 // now the PTRs
2084 storeChangedPTRs(B, new_ptrs);
2085
2086 resp->body = "";
2087 resp->status = 204; // No Content, but indicate success
2088 return;
2089 }
2090
2091 static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
2092 if(req->method != "GET")
2093 throw HttpMethodNotAllowedException();
2094
2095 string q = req->getvars["q"];
2096 string sMax = req->getvars["max"];
2097 string sObjectType = req->getvars["object_type"];
2098
2099 int maxEnts = 100;
2100 int ents = 0;
2101
2102 // the following types of data can be searched for using the api
2103 enum class ObjectType
2104 {
2105 ALL,
2106 ZONE,
2107 RECORD,
2108 COMMENT
2109 } objectType;
2110
2111 if (q.empty())
2112 throw ApiException("Query q can't be blank");
2113 if (!sMax.empty())
2114 maxEnts = std::stoi(sMax);
2115 if (maxEnts < 1)
2116 throw ApiException("Maximum entries must be larger than 0");
2117
2118 if (sObjectType.empty())
2119 objectType = ObjectType::ALL;
2120 else if (sObjectType == "all")
2121 objectType = ObjectType::ALL;
2122 else if (sObjectType == "zone")
2123 objectType = ObjectType::ZONE;
2124 else if (sObjectType == "record")
2125 objectType = ObjectType::RECORD;
2126 else if (sObjectType == "comment")
2127 objectType = ObjectType::COMMENT;
2128 else
2129 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2130
2131 SimpleMatch sm(q,true);
2132 UeberBackend B;
2133 vector<DomainInfo> domains;
2134 vector<DNSResourceRecord> result_rr;
2135 vector<Comment> result_c;
2136 map<int,DomainInfo> zoneIdZone;
2137 map<int,DomainInfo>::iterator val;
2138 Json::array doc;
2139
2140 B.getAllDomains(&domains, true);
2141
2142 for(const DomainInfo di: domains)
2143 {
2144 if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && sm.match(di.zone)) {
2145 doc.push_back(Json::object {
2146 { "object_type", "zone" },
2147 { "zone_id", apiZoneNameToId(di.zone) },
2148 { "name", di.zone.toString() }
2149 });
2150 ents++;
2151 }
2152 zoneIdZone[di.id] = di; // populate cache
2153 }
2154
2155 if ((objectType == ObjectType::ALL || objectType == ObjectType::RECORD) && B.searchRecords(q, maxEnts, result_rr))
2156 {
2157 for(const DNSResourceRecord& rr: result_rr)
2158 {
2159 if (!rr.qtype.getCode())
2160 continue; // skip empty non-terminals
2161
2162 auto object = Json::object {
2163 { "object_type", "record" },
2164 { "name", rr.qname.toString() },
2165 { "type", rr.qtype.getName() },
2166 { "ttl", (double)rr.ttl },
2167 { "disabled", rr.disabled },
2168 { "content", makeApiRecordContent(rr.qtype, rr.content) }
2169 };
2170 if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
2171 object["zone_id"] = apiZoneNameToId(val->second.zone);
2172 object["zone"] = val->second.zone.toString();
2173 }
2174 doc.push_back(object);
2175 }
2176 }
2177
2178 if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && B.searchComments(q, maxEnts, result_c))
2179 {
2180 for(const Comment &c: result_c)
2181 {
2182 auto object = Json::object {
2183 { "object_type", "comment" },
2184 { "name", c.qname.toString() },
2185 { "content", c.content }
2186 };
2187 if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
2188 object["zone_id"] = apiZoneNameToId(val->second.zone);
2189 object["zone"] = val->second.zone.toString();
2190 }
2191 doc.push_back(object);
2192 }
2193 }
2194
2195 resp->setBody(doc);
2196 }
2197
2198 void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
2199 if(req->method != "PUT")
2200 throw HttpMethodNotAllowedException();
2201
2202 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
2203
2204 uint64_t count = purgeAuthCachesExact(canon);
2205 resp->setBody(Json::object {
2206 { "count", (int) count },
2207 { "result", "Flushed cache." }
2208 });
2209 }
2210
2211 void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
2212 {
2213 resp->headers["Cache-Control"] = "max-age=86400";
2214 resp->headers["Content-Type"] = "text/css";
2215
2216 ostringstream ret;
2217 ret<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl;
2218 ret<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl;
2219 ret<<"a { color: #0959c2; }"<<endl;
2220 ret<<"a:hover { color: #3B8EC8; }"<<endl;
2221 ret<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl;
2222 ret<<".row:before, .row:after { display: table; content:\" \"; }"<<endl;
2223 ret<<".row:after { clear: both; }"<<endl;
2224 ret<<".columns { position: relative; min-height: 1px; float: left; }"<<endl;
2225 ret<<".all { width: 100%; }"<<endl;
2226 ret<<".headl { width: 60%; }"<<endl;
2227 ret<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2228 ret<<"background-image: url(data:image/png;base64,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);";
2229 ret<<" width: 154px; height: 20px; }"<<endl;
2230 ret<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl;
2231 ret<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl;
2232 ret<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl;
2233 ret<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl;
2234 ret<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl;
2235 ret<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl;
2236 ret<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl;
2237 ret<<"table.data tr:hover { background: white; }"<<endl;
2238 ret<<".ringmeta { margin-bottom: 5px; }"<<endl;
2239 ret<<".resetring {float: right; }"<<endl;
2240 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;
2241 ret<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl;
2242 ret<<".resizering {float: right;}"<<endl;
2243 resp->body = ret.str();
2244 resp->status = 200;
2245 }
2246
2247 void AuthWebServer::webThread()
2248 {
2249 try {
2250 setThreadName("pdns/webserver");
2251 if(::arg().mustDo("api")) {
2252 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
2253 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
2254 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
2255 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics);
2256 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail);
2257 d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys);
2258 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
2259 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
2260 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
2261 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport);
2262 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind);
2263 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata);
2264 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
2265 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify);
2266 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
2267 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
2268 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail);
2269 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
2270 d_ws->registerApiHandler("/api", &apiDiscovery);
2271 }
2272 if (::arg().mustDo("webserver")) {
2273 d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
2274 d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
2275 }
2276 d_ws->go();
2277 }
2278 catch(...) {
2279 g_log<<Logger::Error<<"AuthWebServer thread caught an exception, dying"<<endl;
2280 _exit(1);
2281 }
2282 }