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