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