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