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