2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
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.
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.
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.
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.
26 #include "dynlistener.hh"
29 #include "webserver.hh"
34 #include "arguments.hh"
37 #include "ueberbackend.hh"
38 #include <boost/format.hpp>
40 #include "namespaces.hh"
43 #include "dnsseckeeper.hh"
45 #include "zoneparser-tng.hh"
46 #include "common_startup.hh"
47 #include "auth-caches.hh"
48 #include "threadname.hh"
49 #include "tsigutils.hh"
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
);
59 AuthWebServer::AuthWebServer()
62 d_min10
=d_min5
=d_min1
=0;
65 if(arg().mustDo("webserver") || arg().mustDo("api")) {
66 d_ws
= new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
67 d_ws
->setApiKey(arg()["api-key"]);
68 d_ws
->setPassword(arg()["webserver-password"]);
71 acl
.toMasks(::arg()["webserver-allow-from"]);
78 void AuthWebServer::go()
81 pthread_create(&d_tid
, 0, webThreadHelper
, this);
82 pthread_create(&d_tid
, 0, statThreadHelper
, this);
85 void AuthWebServer::statThread()
88 setThreadName("pdns/statHelper");
90 d_queries
.submit(S
.read("udp-queries"));
91 d_cachehits
.submit(S
.read("packetcache-hit"));
92 d_cachemisses
.submit(S
.read("packetcache-miss"));
93 d_qcachehits
.submit(S
.read("query-cache-hit"));
94 d_qcachemisses
.submit(S
.read("query-cache-miss"));
99 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
104 void *AuthWebServer::statThreadHelper(void *p
)
106 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
108 return 0; // never reached
111 void *AuthWebServer::webThreadHelper(void *p
)
113 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
115 return 0; // never reached
118 static string
htmlescape(const string
&s
) {
120 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
141 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
145 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
147 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
152 ret
<<"<div class=\"panel\">";
153 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
154 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
155 ret
<<"<div class=ringmeta>";
156 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
157 ret
<<"<span class=resizering>Resize: ";
158 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
159 for(int i
=0;sizes
[i
];++i
) {
160 if(S
.getRingSize(ringname
)!=sizes
[i
])
161 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
163 ret
<<"("<<sizes
[i
]<<") ";
165 ret
<<"</span></div>";
167 ret
<<"<table class=\"data\">";
169 int total
=max(1,tot
);
170 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
171 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
174 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
176 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
;
178 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
179 ret
<<"</table></div>"<<endl
;
182 void AuthWebServer::printvars(ostringstream
&ret
)
184 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
186 vector
<string
>entries
=S
.getEntries();
187 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
188 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
191 ret
<<"</table></div>"<<endl
;
194 void AuthWebServer::printargs(ostringstream
&ret
)
196 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
198 vector
<string
>entries
=arg().list();
199 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
200 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
204 string
AuthWebServer::makePercentage(const double& val
)
206 return (boost::format("%.01f%%") % val
).str();
209 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
211 if(!req
->getvars
["resetring"].empty()) {
212 if (S
.ringExists(req
->getvars
["resetring"]))
213 S
.resetRing(req
->getvars
["resetring"]);
215 resp
->headers
["Location"] = req
->url
.path
;
218 if(!req
->getvars
["resizering"].empty()){
219 int size
=std::stoi(req
->getvars
["size"]);
220 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
221 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
223 resp
->headers
["Location"] = req
->url
.path
;
229 ret
<<"<!DOCTYPE html>"<<endl
;
230 ret
<<"<html><head>"<<endl
;
231 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
232 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
233 ret
<<"</head><body>"<<endl
;
235 ret
<<"<div class=\"row\">"<<endl
;
236 ret
<<"<div class=\"headl columns\">";
237 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
238 if(!arg()["config-name"].empty()) {
239 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
241 ret
<<"</a></div>"<<endl
;
242 ret
<<"<div class=\"headr columns\"></div></div>";
243 ret
<<"<div class=\"row\"><div class=\"all columns\">";
245 time_t passed
=time(0)-s_starttime
;
248 humanDuration(passed
)<<
251 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
252 (int)d_queries
.get1()<<", "<<
253 (int)d_queries
.get5()<<", "<<
254 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
257 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
258 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
259 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
260 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
261 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
264 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
265 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
266 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
267 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
268 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
271 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
272 (int)d_qcachemisses
.get1()<<", "<<
273 (int)d_qcachemisses
.get5()<<", "<<
274 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
277 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
278 if(req
->getvars
["ring"].empty()) {
279 vector
<string
>entries
=S
.listRings();
280 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
)
281 printtable(ret
,*i
,S
.getRingTitle(*i
));
284 if(arg().mustDo("webserver-print-arguments"))
287 else if(S
.ringExists(req
->getvars
["ring"]))
288 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
290 ret
<<"</div></div>"<<endl
;
291 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
292 ret
<<"</body></html>"<<endl
;
294 resp
->body
= ret
.str();
298 /** Helper to build a record content as needed. */
299 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
300 // noDot: for backend storage, pass true. for API users, pass false.
301 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
302 return drc
->getZoneRepresentation(noDot
);
305 /** "Normalize" record content for API consumers. */
306 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
307 return makeRecordContent(qtype
, content
, false);
310 /** "Normalize" record content for backend storage. */
311 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
312 return makeRecordContent(qtype
, content
, true);
315 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
*dk
) {
316 string zoneId
= apiZoneNameToId(di
.zone
);
317 vector
<string
> masters
;
318 for(const auto& m
: di
.masters
)
319 masters
.push_back(m
.toStringWithPortExcept(53));
321 return Json::object
{
322 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
324 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
325 { "name", di
.zone
.toString() },
326 { "kind", di
.getKindString() },
327 { "dnssec", dk
->isSecuredZone(di
.zone
) },
328 { "account", di
.account
},
329 { "masters", masters
},
330 { "serial", (double)di
.serial
},
331 { "notified_serial", (double)di
.notified_serial
},
332 { "last_check", (double)di
.last_check
}
336 static bool shouldDoRRSets(HttpRequest
* req
) {
337 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
339 if (req
->getvars
["rrsets"] == "false")
341 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
344 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
347 if(!B
.getDomainInfo(zonename
, di
)) {
348 throw HttpNotFoundException();
352 Json::object doc
= getZoneInfo(di
, &dk
);
353 // extra stuff getZoneInfo doesn't do for us (more expensive)
355 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
356 doc
["soa_edit_api"] = soa_edit_api
;
358 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
359 doc
["soa_edit"] = soa_edit
;
361 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
362 doc
["nsec3param"] = nsec3param
;
364 bool nsec3narrowbool
= false;
365 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
366 if (nsec3narrow
== "1")
367 nsec3narrowbool
= true;
368 doc
["nsec3narrow"] = nsec3narrowbool
;
371 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
372 doc
["api_rectify"] = (api_rectify
== "1");
375 vector
<string
> tsig_master
, tsig_slave
;
376 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
377 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
379 Json::array tsig_master_keys
;
380 for (const auto& keyname
: tsig_master
) {
381 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
383 doc
["master_tsig_key_ids"] = tsig_master_keys
;
385 Json::array tsig_slave_keys
;
386 for (const auto& keyname
: tsig_slave
) {
387 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
389 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
392 vector
<DNSResourceRecord
> records
;
393 vector
<Comment
> comments
;
395 // load all records + sort
397 DNSResourceRecord rr
;
398 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
399 while(di
.backend
->get(rr
)) {
400 if (!rr
.qtype
.getCode())
401 continue; // skip empty non-terminals
402 records
.push_back(rr
);
404 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
405 /* if you ever want to update this comparison function,
406 please be aware that you will also need to update the conditions in the code merging
407 the records and comments below */
408 if (a
.qname
== b
.qname
) {
409 return b
.qtype
< a
.qtype
;
411 return b
.qname
< a
.qname
;
415 // load all comments + sort
418 di
.backend
->listComments(di
.id
);
419 while(di
.backend
->getComment(comment
)) {
420 comments
.push_back(comment
);
422 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
423 /* if you ever want to update this comparison function,
424 please be aware that you will also need to update the conditions in the code merging
425 the records and comments below */
426 if (a
.qname
== b
.qname
) {
427 return b
.qtype
< a
.qtype
;
429 return b
.qname
< a
.qname
;
435 Json::array rrset_records
;
436 Json::array rrset_comments
;
437 DNSName current_qname
;
440 auto rit
= records
.begin();
441 auto cit
= comments
.begin();
443 while (rit
!= records
.end() || cit
!= comments
.end()) {
444 // 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
445 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
446 current_qname
= rit
->qname
;
447 current_qtype
= rit
->qtype
;
450 current_qname
= cit
->qname
;
451 current_qtype
= cit
->qtype
;
455 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
456 ttl
= min(ttl
, rit
->ttl
);
457 rrset_records
.push_back(Json::object
{
458 { "disabled", rit
->disabled
},
459 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
463 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
464 rrset_comments
.push_back(Json::object
{
465 { "modified_at", (double)cit
->modified_at
},
466 { "account", cit
->account
},
467 { "content", cit
->content
}
472 rrset
["name"] = current_qname
.toString();
473 rrset
["type"] = current_qtype
.getName();
474 rrset
["records"] = rrset_records
;
475 rrset
["comments"] = rrset_comments
;
476 rrset
["ttl"] = (double)ttl
;
477 rrsets
.push_back(rrset
);
479 rrset_records
.clear();
480 rrset_comments
.clear();
483 doc
["rrsets"] = rrsets
;
489 void productServerStatisticsFetch(map
<string
,string
>& out
)
491 vector
<string
> items
= S
.getEntries();
492 for(const string
& item
: items
) {
493 out
[item
] = std::to_string(S
.read(item
));
497 out
["uptime"] = std::to_string(time(0) - s_starttime
);
500 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
501 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
502 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
506 static void gatherRecords(const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
507 static const std::set
<uint16_t> onlyOneEntryTypes
= { QType::CNAME
, QType::SOA
};
509 DNSResourceRecord rr
;
515 validateGatheredRRType(rr
);
516 const auto& items
= container
["records"].array_items();
517 if (onlyOneEntryTypes
.count(qtype
.getCode()) != 0 && items
.size() > 1) {
518 throw ApiException("RRset for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" has more than one record");
521 for(const auto& record
: items
) {
522 string content
= stringFromJson(record
, "content");
523 rr
.disabled
= boolFromJson(record
, "disabled");
525 // validate that the client sent something we can actually parse, and require that data to be dotted.
527 if (rr
.qtype
.getCode() != QType::AAAA
) {
528 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
529 if (!pdns_iequals(tmp
, content
)) {
530 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
533 struct in6_addr tmpbuf
;
534 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
535 throw std::runtime_error("Invalid IPv6 address");
538 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
540 catch(std::exception
& e
)
542 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
545 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
546 boolFromJson(record
, "set-ptr", false) == true) {
547 DNSResourceRecord ptr
;
550 // verify that there's a zone for the PTR
552 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
553 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
555 ptr
.domain_id
= sd
.domain_id
;
556 new_ptrs
.push_back(ptr
);
559 new_records
.push_back(rr
);
563 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
568 time_t now
= time(0);
569 for (auto comment
: container
["comments"].array_items()) {
570 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
571 c
.content
= stringFromJson(comment
, "content");
572 c
.account
= stringFromJson(comment
, "account");
573 new_comments
.push_back(c
);
577 static void checkDefaultDNSSECAlgos() {
578 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
579 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
580 int k_size
= arg().asNum("default-ksk-size");
581 int z_size
= arg().asNum("default-zsk-size");
583 // Sanity check DNSSEC parameters
584 if (::arg()["default-zsk-algorithm"] != "") {
586 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
587 else if (k_algo
<= 10 && k_size
== 0)
588 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
591 if (::arg()["default-zsk-algorithm"] != "") {
593 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
594 else if (z_algo
<= 10 && z_size
== 0)
595 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
599 static void throwUnableToSecure(const DNSName
& zonename
) {
600 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
601 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
604 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
) {
606 bool shouldRectify
= false;
607 for(auto value
: document
["masters"].array_items()) {
608 string master
= value
.string_value();
610 throw ApiException("Master can not be an empty string");
611 zonemaster
+= master
+ " ";
614 if (zonemaster
!= "") {
615 di
.backend
->setMaster(zonename
, zonemaster
);
617 if (document
["kind"].is_string()) {
618 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
620 if (document
["soa_edit_api"].is_string()) {
621 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
623 if (document
["soa_edit"].is_string()) {
624 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
627 bool api_rectify
= boolFromJson(document
, "api_rectify");
628 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
630 catch (const JsonException
&) {}
632 if (document
["account"].is_string()) {
633 di
.backend
->setAccount(zonename
, document
["account"].string_value());
637 bool dnssecInJSON
= false;
638 bool dnssecDocVal
= false;
641 dnssecDocVal
= boolFromJson(document
, "dnssec");
644 catch (const JsonException
&) {}
646 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
651 checkDefaultDNSSECAlgos();
653 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
654 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
655 int k_size
= arg().asNum("default-ksk-size");
656 int z_size
= arg().asNum("default-zsk-size");
660 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
661 throwUnableToSecure(zonename
);
667 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
668 throwUnableToSecure(zonename
);
672 // Used later for NSEC3PARAM
673 isDNSSECZone
= dk
.isSecuredZone(zonename
);
676 throwUnableToSecure(zonename
);
678 shouldRectify
= true;
681 // "dnssec": false in json
684 if (!dk
.unSecureZone(zonename
, error
, info
)) {
685 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
687 isDNSSECZone
= dk
.isSecuredZone(zonename
);
689 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
691 shouldRectify
= true;
696 if(document
["nsec3param"].string_value().length() > 0) {
697 shouldRectify
= true;
698 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
699 string error_msg
= "";
701 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
703 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
704 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
706 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
707 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
708 "' passed our basic sanity checks, but cannot be used with the current backend.");
712 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
715 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
716 if (api_rectify
.empty()) {
717 if (::arg().mustDo("default-api-rectify")) {
721 if (api_rectify
== "1") {
724 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true)) {
725 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
730 string soa_edit_api_kind
;
731 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
732 if (!soa_edit_api_kind
.empty()) {
734 if (!B
.getSOAUncached(zonename
, sd
))
737 string soa_edit_kind
;
738 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
740 DNSResourceRecord rr
;
741 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
742 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
743 throw ApiException("Hosting backend does not support editing records.");
749 if (!document
["master_tsig_key_ids"].is_null()) {
750 vector
<string
> metadata
;
753 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
754 auto keyname(apiZoneIdToName(value
.string_value()));
755 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
756 if (keyAlgo
.empty() || keyContent
.empty()) {
757 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
759 metadata
.push_back(keyname
.toString());
761 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
762 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
765 if (!document
["slave_tsig_key_ids"].is_null()) {
766 vector
<string
> metadata
;
769 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
770 auto keyname(apiZoneIdToName(value
.string_value()));
771 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
772 if (keyAlgo
.empty() || keyContent
.empty()) {
773 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
775 metadata
.push_back(keyname
.toString());
777 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
778 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
783 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
784 static vector
<string
> builtinOptions
{
787 "ALLOW-DNSUPDATE-FROM",
788 "TSIG-ALLOW-DNSUPDATE",
790 "SOA-EDIT-DNSUPDATE",
794 "GSS-ALLOW-AXFR-PRINCIPAL",
795 "GSS-ACCEPTOR-PRINCIPAL",
805 "TSIG-ALLOW-DNSUPDATE"
808 // the following options do not allow modifications via API
809 static vector
<string
> protectedOptions
{
819 if (kind
.find("X-") == 0)
824 for (const string
& s
: builtinOptions
) {
826 for (const string
& s2
: protectedOptions
) {
827 if (!readonly
&& s
== s2
)
838 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
839 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
843 if (!B
.getDomainInfo(zonename
, di
)) {
844 throw HttpNotFoundException();
847 if (req
->method
== "GET") {
848 map
<string
, vector
<string
> > md
;
849 Json::array document
;
851 if (!B
.getAllDomainMetadata(zonename
, md
))
852 throw HttpNotFoundException();
854 for (const auto& i
: md
) {
856 for (string j
: i
.second
)
857 entries
.push_back(j
);
860 { "type", "Metadata" },
862 { "metadata", entries
}
865 document
.push_back(key
);
868 resp
->setBody(document
);
869 } else if (req
->method
== "POST") {
870 auto document
= req
->json();
872 vector
<string
> entries
;
875 kind
= stringFromJson(document
, "kind");
876 } catch (const JsonException
&) {
877 throw ApiException("kind is not specified or not a string");
880 if (!isValidMetadataKind(kind
, false))
881 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
883 vector
<string
> vecMetadata
;
885 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
886 throw ApiException("Could not retrieve metadata entries for domain '" +
887 zonename
.toString() + "'");
889 auto& metadata
= document
["metadata"];
890 if (!metadata
.is_array())
891 throw ApiException("metadata is not specified or not an array");
893 for (const auto& i
: metadata
.array_items()) {
895 throw ApiException("metadata must be strings");
896 else if (std::find(vecMetadata
.cbegin(),
898 i
.string_value()) == vecMetadata
.cend()) {
899 vecMetadata
.push_back(i
.string_value());
903 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
904 throw ApiException("Could not update metadata entries for domain '" +
905 zonename
.toString() + "'");
907 Json::array respMetadata
;
908 for (const string
& s
: vecMetadata
)
909 respMetadata
.push_back(s
);
912 { "type", "Metadata" },
913 { "kind", document
["kind"] },
914 { "metadata", respMetadata
}
920 throw HttpMethodNotAllowedException();
923 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
924 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
928 if (!B
.getDomainInfo(zonename
, di
)) {
929 throw HttpNotFoundException();
932 string kind
= req
->parameters
["kind"];
934 if (req
->method
== "GET") {
935 vector
<string
> metadata
;
936 Json::object document
;
939 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
940 throw HttpNotFoundException();
941 else if (!isValidMetadataKind(kind
, true))
942 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
944 document
["type"] = "Metadata";
945 document
["kind"] = kind
;
947 for (const string
& i
: metadata
)
948 entries
.push_back(i
);
950 document
["metadata"] = entries
;
951 resp
->setBody(document
);
952 } else if (req
->method
== "PUT") {
953 auto document
= req
->json();
955 if (!isValidMetadataKind(kind
, false))
956 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
958 vector
<string
> vecMetadata
;
959 auto& metadata
= document
["metadata"];
960 if (!metadata
.is_array())
961 throw ApiException("metadata is not specified or not an array");
963 for (const auto& i
: metadata
.array_items()) {
965 throw ApiException("metadata must be strings");
966 vecMetadata
.push_back(i
.string_value());
969 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
970 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
973 { "type", "Metadata" },
975 { "metadata", metadata
}
979 } else if (req
->method
== "DELETE") {
980 if (!isValidMetadataKind(kind
, false))
981 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
983 vector
<string
> md
; // an empty vector will do it
984 if (!B
.setDomainMetadata(zonename
, kind
, md
))
985 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
987 throw HttpMethodNotAllowedException();
990 // Throws 404 if the key with inquireKeyId does not exist
991 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
992 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
994 for(const auto& value
: keyset
) {
995 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1001 throw HttpNotFoundException();
1005 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1006 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1008 bool inquireSingleKey
= inquireKeyId
>= 0;
1011 for(const auto& value
: keyset
) {
1012 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1017 switch (value
.second
.keyType
) {
1018 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1019 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1020 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1024 { "type", "Cryptokey" },
1025 { "id", (int)value
.second
.id
},
1026 { "active", value
.second
.active
},
1027 { "keytype", keyType
},
1028 { "flags", (uint16_t)value
.first
.d_flags
},
1029 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1030 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1031 { "bits", value
.first
.getKey()->getBits() }
1034 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1036 for(const uint8_t keyid
: { DNSSECKeeper::SHA1
, DNSSECKeeper::SHA256
, DNSSECKeeper::GOST
, DNSSECKeeper::SHA384
})
1038 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1043 if (inquireSingleKey
) {
1044 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1051 if (inquireSingleKey
) {
1052 // we came here because we couldn't find the requested key.
1053 throw HttpNotFoundException();
1060 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1061 * It deletes a key from :zone_name specified by :cryptokey_id.
1063 * Case 1: the backend returns true on removal. This means the key is gone.
1064 * The server returns 204 No Content, no body.
1065 * Case 2: the backend returns false on removal. An error occurred.
1066 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1067 * Case 3: the key or zone does not exist.
1068 * The server returns 404 Not Found
1070 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1071 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1075 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1080 * This method adds a key to a zone by generate it or content parameter.
1083 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1084 * "keytype" : "ksk|zsk" <string>
1085 * "active" : "true|false" <value>
1086 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1087 * "bits" : number of bits <int>
1091 * Case 1: keytype isn't ksk|zsk
1092 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1093 * Case 2: 'bits' must be a positive integer value.
1094 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1095 * Case 3: The "algorithm" isn't supported
1096 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1097 * Case 4: Algorithm <= 10 and no bits were passed
1098 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1099 * Case 5: The wrong keysize was passed
1100 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1101 * Case 6: If the server cant guess the keysize
1102 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1103 * Case 7: The key-creation failed
1104 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1105 * Case 8: The key in content has the wrong format
1106 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1107 * Case 9: The wrong combination of fields is submitted
1108 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1109 * Case 10: No content and everything was fine
1110 * The server returns 201 Created and all public data about the new cryptokey
1111 * Case 11: With specified content
1112 * The server returns 201 Created and all public data about the added cryptokey
1115 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1116 auto document
= req
->json();
1117 string privatekey_fieldname
= "privatekey";
1118 auto privatekey
= document
["privatekey"];
1119 if (privatekey
.is_null()) {
1120 // Fallback to the old "content" behaviour
1121 privatekey
= document
["content"];
1122 privatekey_fieldname
= "content";
1124 bool active
= boolFromJson(document
, "active", false);
1127 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1129 } else if (stringFromJson(document
, "keytype") == "zsk") {
1132 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1135 int64_t insertedId
= -1;
1137 if (privatekey
.is_null()) {
1138 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1139 auto docbits
= document
["bits"];
1140 if (!docbits
.is_null()) {
1141 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1142 throw ApiException("'bits' must be a positive integer value");
1144 bits
= docbits
.int_value();
1147 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1148 auto providedAlgo
= document
["algorithm"];
1149 if (providedAlgo
.is_string()) {
1150 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1151 if (algorithm
== -1)
1152 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1153 } else if (providedAlgo
.is_number()) {
1154 algorithm
= providedAlgo
.int_value();
1155 } else if (!providedAlgo
.is_null()) {
1156 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1160 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
)) {
1161 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1163 } catch (std::runtime_error
& error
) {
1164 throw ApiException(error
.what());
1167 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1168 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1169 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1170 DNSKEYRecordContent dkrc
;
1171 DNSSECPrivateKey dpk
;
1173 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1174 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1175 // TODO remove in 4.2.0
1176 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1177 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1186 catch (std::runtime_error
& error
) {
1187 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1189 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
)) {
1190 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1192 } catch (std::runtime_error
& error
) {
1193 throw ApiException(error
.what());
1196 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1198 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1200 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1205 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1206 * It de/activates a key from :zone_name specified by :cryptokey_id.
1208 * Case 1: invalid JSON data
1209 * The server returns 400 Bad Request
1210 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1211 * The server returns 204 No Content
1212 * Case 3: the backend returns false on de/activation. An error occurred.
1213 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1215 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1216 //throws an exception if the Body is empty
1217 auto document
= req
->json();
1218 //throws an exception if the key does not exist or is not a bool
1219 bool active
= boolFromJson(document
, "active");
1221 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1222 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1226 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1227 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1237 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1238 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1239 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1241 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1242 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1245 DNSSECKeeper
dk(&B
);
1247 if (!B
.getDomainInfo(zonename
, di
)) {
1248 throw HttpNotFoundException();
1251 int inquireKeyId
= -1;
1252 if (req
->parameters
.count("key_id")) {
1253 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1254 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1257 if (req
->method
== "GET") {
1258 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1259 } else if (req
->method
== "DELETE") {
1260 if (inquireKeyId
== -1)
1261 throw HttpBadRequestException();
1262 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1263 } else if (req
->method
== "POST") {
1264 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1265 } else if (req
->method
== "PUT") {
1266 if (inquireKeyId
== -1)
1267 throw HttpBadRequestException();
1268 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1270 throw HttpMethodNotAllowedException(); //Returns method not allowed
1274 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1275 DNSResourceRecord rr
;
1276 vector
<string
> zonedata
;
1277 stringtok(zonedata
, zonestring
, "\r\n");
1279 ZoneParserTNG
zpt(zonedata
, zonename
);
1283 string comment
= "Imported via the API";
1286 while(zpt
.get(rr
, &comment
)) {
1287 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1289 if(rr
.qtype
.getCode() == QType::SOA
)
1291 validateGatheredRRType(rr
);
1293 new_records
.push_back(rr
);
1296 catch(std::exception
& ae
) {
1297 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1301 /** Throws ApiException if records with duplicate name/type/content are present.
1302 * NOTE: sorts records in-place.
1304 static void checkDuplicateRecords(vector
<DNSResourceRecord
>& records
) {
1305 sort(records
.begin(), records
.end(),
1306 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1307 /* we need _strict_ weak ordering */
1308 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1311 DNSResourceRecord previous
;
1312 for(const auto& rec
: records
) {
1313 if (previous
.qtype
== rec
.qtype
&& previous
.qname
== rec
.qname
&& previous
.content
== rec
.content
) {
1314 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1320 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1322 string contentFromDB
;
1323 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1324 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1325 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1329 if (!getTSIGHashEnum(algo
, the
)) {
1330 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1334 if (B64Decode(content
, b64out
) == -1) {
1335 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1339 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1340 Json::object tsigkey
= {
1341 { "name", keyname
.toStringNoDot() },
1342 { "id", apiZoneNameToId(keyname
) },
1343 { "algorithm", algo
.toStringNoDot() },
1345 { "type", "TSIGKey" }
1350 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1351 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1354 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1356 if (req
->method
== "GET") {
1357 vector
<struct TSIGKey
> keys
;
1359 if (!B
.getTSIGKeys(keys
)) {
1360 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1365 for(const auto &key
: keys
) {
1366 doc
.push_back(makeJSONTSIGKey(key
, false));
1369 } else if (req
->method
== "POST") {
1370 auto document
= req
->json();
1371 DNSName
keyname(stringFromJson(document
, "name"));
1372 DNSName
algo(stringFromJson(document
, "algorithm"));
1373 string content
= document
["key"].string_value();
1375 if (content
.empty()) {
1377 content
= makeTSIGKey(algo
);
1378 } catch (const PDNSException
& e
) {
1379 throw HttpBadRequestException(e
.reason
);
1383 // Will throw an ApiException or HttpConflictException on error
1384 checkTSIGKey(B
, keyname
, algo
, content
);
1386 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1387 throw HttpInternalServerErrorException("Unable to add TSIG key");
1391 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1393 throw HttpMethodNotAllowedException();
1397 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1399 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1403 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1404 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1409 tsk
.algorithm
= algo
;
1412 if (req
->method
== "GET") {
1413 resp
->setBody(makeJSONTSIGKey(tsk
));
1414 } else if (req
->method
== "PUT") {
1415 json11::Json document
;
1416 if (!req
->body
.empty()) {
1417 document
= req
->json();
1419 if (document
["name"].is_string()) {
1420 tsk
.name
= DNSName(document
["name"].string_value());
1422 if (document
["algorithm"].is_string()) {
1423 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1426 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1427 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1430 if (document
["key"].is_string()) {
1431 string new_content
= document
["key"].string_value();
1433 if (B64Decode(new_content
, decoded
) == -1) {
1434 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1436 tsk
.key
= new_content
;
1438 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1439 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1441 if (tsk
.name
!= keyname
) {
1442 // Remove the old key
1443 if (!B
.deleteTSIGKey(keyname
)) {
1444 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1447 resp
->setBody(makeJSONTSIGKey(tsk
));
1448 } else if (req
->method
== "DELETE") {
1449 if (!B
.deleteTSIGKey(keyname
)) {
1450 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1456 throw HttpMethodNotAllowedException();
1460 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1462 DNSSECKeeper
dk(&B
);
1463 if (req
->method
== "POST") {
1465 auto document
= req
->json();
1466 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1467 apiCheckNameAllowedCharacters(zonename
.toString());
1468 zonename
.makeUsLowerCase();
1470 bool exists
= B
.getDomainInfo(zonename
, di
);
1472 throw HttpConflictException();
1474 // validate 'kind' is set
1475 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1477 string zonestring
= document
["zone"].string_value();
1478 auto rrsets
= document
["rrsets"];
1479 if (rrsets
.is_array() && zonestring
!= "")
1480 throw ApiException("You cannot give rrsets AND zone data as text");
1482 auto nameservers
= document
["nameservers"];
1483 if (!nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1484 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
1486 string soa_edit_api_kind
;
1487 if (document
["soa_edit_api"].is_string()) {
1488 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1491 soa_edit_api_kind
= "DEFAULT";
1493 string soa_edit_kind
= document
["soa_edit"].string_value();
1495 // if records/comments are given, load and check them
1496 bool have_soa
= false;
1497 bool have_zone_ns
= false;
1498 vector
<DNSResourceRecord
> new_records
;
1499 vector
<Comment
> new_comments
;
1500 vector
<DNSResourceRecord
> new_ptrs
;
1502 if (rrsets
.is_array()) {
1503 for (const auto& rrset
: rrsets
.array_items()) {
1504 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1505 apiCheckQNameAllowedCharacters(qname
.toString());
1507 qtype
= stringFromJson(rrset
, "type");
1508 if (qtype
.getCode() == 0) {
1509 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1511 if (rrset
["records"].is_array()) {
1512 int ttl
= intFromJson(rrset
, "ttl");
1513 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1515 if (rrset
["comments"].is_array()) {
1516 gatherComments(rrset
, qname
, qtype
, new_comments
);
1519 } else if (zonestring
!= "") {
1520 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1523 for(auto& rr
: new_records
) {
1524 rr
.qname
.makeUsLowerCase();
1525 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1526 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1527 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1529 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1531 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1533 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1534 have_zone_ns
= true;
1538 // synthesize RRs as needed
1539 DNSResourceRecord autorr
;
1540 autorr
.qname
= zonename
;
1542 autorr
.ttl
= ::arg().asNum("default-ttl");
1544 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1545 // synthesize a SOA record so the zone "really" exists
1546 string soa
= (boost::format("%s %s %ul")
1547 % ::arg()["default-soa-name"]
1548 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1549 % document
["serial"].int_value()
1552 fillSOAData(soa
, sd
); // fills out default values for us
1553 autorr
.qtype
= QType::SOA
;
1554 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1555 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1556 new_records
.push_back(autorr
);
1559 // create NS records if nameservers are given
1560 for (auto value
: nameservers
.array_items()) {
1561 string nameserver
= value
.string_value();
1562 if (nameserver
.empty())
1563 throw ApiException("Nameservers must be non-empty strings");
1564 if (!isCanonical(nameserver
))
1565 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1567 // ensure the name parses
1568 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1570 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1572 autorr
.qtype
= QType::NS
;
1573 new_records
.push_back(autorr
);
1575 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1579 checkDuplicateRecords(new_records
);
1581 if (boolFromJson(document
, "dnssec", false)) {
1582 checkDefaultDNSSECAlgos();
1584 if(document
["nsec3param"].string_value().length() > 0) {
1585 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1586 string error_msg
= "";
1587 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1588 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1593 // no going back after this
1594 if(!B
.createDomain(zonename
))
1595 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1597 if(!B
.getDomainInfo(zonename
, di
))
1598 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1600 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1601 if (!soa_edit_api_kind
.empty()) {
1602 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1605 di
.backend
->startTransaction(zonename
, di
.id
);
1607 for(auto rr
: new_records
) {
1608 rr
.domain_id
= di
.id
;
1609 di
.backend
->feedRecord(rr
, DNSName());
1611 for(Comment
& c
: new_comments
) {
1612 c
.domain_id
= di
.id
;
1613 di
.backend
->feedComment(c
);
1616 updateDomainSettingsFromDocument(B
, di
, zonename
, document
);
1618 di
.backend
->commitTransaction();
1620 storeChangedPTRs(B
, new_ptrs
);
1622 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1627 if(req
->method
!= "GET")
1628 throw HttpMethodNotAllowedException();
1630 vector
<DomainInfo
> domains
;
1632 if (req
->getvars
.count("zone")) {
1633 string zone
= req
->getvars
["zone"];
1634 apiCheckNameAllowedCharacters(zone
);
1635 DNSName zonename
= apiNameToDNSName(zone
);
1636 zonename
.makeUsLowerCase();
1638 if (B
.getDomainInfo(zonename
, di
)) {
1639 domains
.push_back(di
);
1642 B
.getAllDomains(&domains
, true); // incl. disabled
1646 for(const DomainInfo
& di
: domains
) {
1647 doc
.push_back(getZoneInfo(di
, &dk
));
1652 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1653 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1657 if (!B
.getDomainInfo(zonename
, di
)) {
1658 throw HttpNotFoundException();
1661 if(req
->method
== "PUT") {
1662 // update domain settings
1664 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json());
1667 resp
->status
= 204; // No Content, but indicate success
1670 else if(req
->method
== "DELETE") {
1672 if(!di
.backend
->deleteDomain(zonename
))
1673 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1675 // empty body on success
1677 resp
->status
= 204; // No Content: declare that the zone is gone now
1679 } else if (req
->method
== "PATCH") {
1680 patchZone(req
, resp
);
1682 } else if (req
->method
== "GET") {
1683 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1686 throw HttpMethodNotAllowedException();
1689 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1690 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1692 if(req
->method
!= "GET")
1693 throw HttpMethodNotAllowedException();
1699 if (!B
.getDomainInfo(zonename
, di
)) {
1700 throw HttpNotFoundException();
1703 DNSResourceRecord rr
;
1705 di
.backend
->list(zonename
, di
.id
);
1706 while(di
.backend
->get(rr
)) {
1707 if (!rr
.qtype
.getCode())
1708 continue; // skip empty non-terminals
1711 rr
.qname
.toString() << "\t" <<
1714 rr
.qtype
.getName() << "\t" <<
1715 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1719 if (req
->accept_json
) {
1720 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1722 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1723 resp
->body
= ss
.str();
1727 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1728 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1730 if(req
->method
!= "PUT")
1731 throw HttpMethodNotAllowedException();
1735 if (!B
.getDomainInfo(zonename
, di
)) {
1736 throw HttpNotFoundException();
1739 if(di
.masters
.empty())
1740 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1742 random_shuffle(di
.masters
.begin(), di
.masters
.end());
1743 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1744 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1747 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1748 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1750 if(req
->method
!= "PUT")
1751 throw HttpMethodNotAllowedException();
1755 if (!B
.getDomainInfo(zonename
, di
)) {
1756 throw HttpNotFoundException();
1759 if(!Communicator
.notifyDomain(zonename
))
1760 throw ApiException("Failed to add to the queue - see server log");
1762 resp
->setSuccessResult("Notification queued");
1765 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1766 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1768 if(req
->method
!= "PUT")
1769 throw HttpMethodNotAllowedException();
1773 if (!B
.getDomainInfo(zonename
, di
)) {
1774 throw HttpNotFoundException();
1777 DNSSECKeeper
dk(&B
);
1779 if (!dk
.isSecuredZone(zonename
))
1780 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1782 if (di
.kind
== DomainInfo::Slave
)
1783 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1785 string error_msg
= "";
1787 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1788 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1790 resp
->setSuccessResult("Rectified");
1793 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1794 if (rr
.qtype
.getCode() == QType::A
) {
1796 if (!IpToU32(rr
.content
, &ip
)) {
1797 throw ApiException("PTR: Invalid IP address given");
1799 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1800 % ((ip
>> 24) & 0xff)
1801 % ((ip
>> 16) & 0xff)
1802 % ((ip
>> 8) & 0xff)
1805 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1806 ComboAddress
ca(rr
.content
);
1809 for (int octet
= 0; octet
< 16; ++octet
) {
1810 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1811 // this should be impossible: no byte should give more than two digits in hex format
1812 throw PDNSException("Formatting IPv6 address failed");
1814 ss
<< buf
[0] << '.' << buf
[1] << '.';
1816 string tmp
= ss
.str();
1817 tmp
.resize(tmp
.size()-1); // remove last dot
1818 // reverse and append arpa domain
1819 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1821 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1826 ptr
->disabled
= rr
.disabled
;
1827 ptr
->content
= rr
.qname
.toStringRootDot();
1830 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1831 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1833 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1834 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1836 string soa_edit_api_kind
;
1837 string soa_edit_kind
;
1838 bool soa_changed
= false;
1839 DNSResourceRecord soarr
;
1840 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1841 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1842 if (!soa_edit_api_kind
.empty()) {
1843 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1846 sd
.db
->startTransaction(sd
.qname
);
1847 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1848 sd
.db
->abortTransaction();
1849 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1853 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1856 sd
.db
->commitTransaction();
1857 purgeAuthCachesExact(rr
.qname
);
1861 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
) {
1864 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1865 if (!B
.getDomainInfo(zonename
, di
)) {
1866 throw HttpNotFoundException();
1869 vector
<DNSResourceRecord
> new_records
;
1870 vector
<Comment
> new_comments
;
1871 vector
<DNSResourceRecord
> new_ptrs
;
1873 Json document
= req
->json();
1875 auto rrsets
= document
["rrsets"];
1876 if (!rrsets
.is_array())
1877 throw ApiException("No rrsets given in update request");
1879 di
.backend
->startTransaction(zonename
);
1882 string soa_edit_api_kind
;
1883 string soa_edit_kind
;
1884 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1885 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1886 bool soa_edit_done
= false;
1888 set
<pair
<DNSName
, QType
>> seen
;
1890 for (const auto& rrset
: rrsets
.array_items()) {
1891 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1892 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1893 apiCheckQNameAllowedCharacters(qname
.toString());
1895 qtype
= stringFromJson(rrset
, "type");
1896 if (qtype
.getCode() == 0) {
1897 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1900 if(seen
.count({qname
, qtype
}))
1902 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1904 seen
.insert({qname
, qtype
});
1906 if (changetype
== "DELETE") {
1907 // delete all matching qname/qtype RRs (and, implicitly comments).
1908 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1909 throw ApiException("Hosting backend does not support editing records.");
1912 else if (changetype
== "REPLACE") {
1913 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1914 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
1915 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
1917 bool replace_records
= rrset
["records"].is_array();
1918 bool replace_comments
= rrset
["comments"].is_array();
1920 if (!replace_records
&& !replace_comments
) {
1921 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
1924 new_records
.clear();
1925 new_comments
.clear();
1927 if (replace_records
) {
1928 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1929 int ttl
= intFromJson(rrset
, "ttl");
1930 // new_ptrs is merged.
1931 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1933 for(DNSResourceRecord
& rr
: new_records
) {
1934 rr
.domain_id
= di
.id
;
1935 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1936 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1939 // Check if the DNSNames that should be hostnames, are hostnames
1941 checkHostnameCorrectness(rr
);
1942 } catch (const std::exception
& e
) {
1943 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName() + " " + e
.what());
1946 checkDuplicateRecords(new_records
);
1949 if (replace_comments
) {
1950 gatherComments(rrset
, qname
, qtype
, new_comments
);
1952 for(Comment
& c
: new_comments
) {
1953 c
.domain_id
= di
.id
;
1957 if (replace_records
) {
1958 bool ent_present
= false;
1959 di
.backend
->lookup(QType(QType::ANY
), qname
);
1960 DNSResourceRecord rr
;
1961 while (di
.backend
->get(rr
)) {
1962 if (qtype
.getCode() == 0) {
1965 if (qtype
.getCode() == QType::CNAME
&& rr
.qtype
.getCode() != QType::CNAME
) {
1966 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing non-CNAME RRset");
1967 } else if (qtype
.getCode() != QType::CNAME
&& rr
.qtype
.getCode() == QType::CNAME
) {
1968 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing CNAME RRset");
1972 if (!new_records
.empty() && ent_present
) {
1974 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
1975 throw ApiException("Hosting backend does not support editing records.");
1978 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
1979 throw ApiException("Hosting backend does not support editing records.");
1982 if (replace_comments
) {
1983 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
1984 throw ApiException("Hosting backend does not support editing comments.");
1989 throw ApiException("Changetype not understood");
1992 // edit SOA (if needed)
1993 if (!soa_edit_api_kind
.empty() && !soa_edit_done
) {
1995 if (!B
.getSOAUncached(zonename
, sd
))
1996 throw ApiException("No SOA found for domain '"+zonename
.toString()+"'");
1998 DNSResourceRecord rr
;
1999 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2000 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2001 throw ApiException("Hosting backend does not support editing records.");
2005 // return old and new serials in headers
2006 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2007 fillSOAData(rr
.content
, sd
);
2008 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2012 di
.backend
->abortTransaction();
2016 DNSSECKeeper
dk(&B
);
2018 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
2019 if (dk
.isSecuredZone(zonename
) && !dk
.isPresigned(zonename
) && api_rectify
== "1") {
2020 string error_msg
= "";
2022 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false))
2023 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2026 di
.backend
->commitTransaction();
2028 purgeAuthCachesExact(zonename
);
2031 storeChangedPTRs(B
, new_ptrs
);
2034 resp
->status
= 204; // No Content, but indicate success
2038 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2039 if(req
->method
!= "GET")
2040 throw HttpMethodNotAllowedException();
2042 string q
= req
->getvars
["q"];
2043 string sMax
= req
->getvars
["max"];
2048 throw ApiException("Query q can't be blank");
2050 maxEnts
= std::stoi(sMax
);
2052 throw ApiException("Maximum entries must be larger than 0");
2054 SimpleMatch
sm(q
,true);
2056 vector
<DomainInfo
> domains
;
2057 vector
<DNSResourceRecord
> result_rr
;
2058 vector
<Comment
> result_c
;
2059 map
<int,DomainInfo
> zoneIdZone
;
2060 map
<int,DomainInfo
>::iterator val
;
2063 B
.getAllDomains(&domains
, true);
2065 for(const DomainInfo di
: domains
)
2067 if (ents
< maxEnts
&& sm
.match(di
.zone
)) {
2068 doc
.push_back(Json::object
{
2069 { "object_type", "zone" },
2070 { "zone_id", apiZoneNameToId(di
.zone
) },
2071 { "name", di
.zone
.toString() }
2075 zoneIdZone
[di
.id
] = di
; // populate cache
2078 if (B
.searchRecords(q
, maxEnts
, result_rr
))
2080 for(const DNSResourceRecord
& rr
: result_rr
)
2082 if (!rr
.qtype
.getCode())
2083 continue; // skip empty non-terminals
2085 auto object
= Json::object
{
2086 { "object_type", "record" },
2087 { "name", rr
.qname
.toString() },
2088 { "type", rr
.qtype
.getName() },
2089 { "ttl", (double)rr
.ttl
},
2090 { "disabled", rr
.disabled
},
2091 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2093 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2094 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2095 object
["zone"] = val
->second
.zone
.toString();
2097 doc
.push_back(object
);
2101 if (B
.searchComments(q
, maxEnts
, result_c
))
2103 for(const Comment
&c
: result_c
)
2105 auto object
= Json::object
{
2106 { "object_type", "comment" },
2107 { "name", c
.qname
.toString() },
2108 { "content", c
.content
}
2110 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2111 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2112 object
["zone"] = val
->second
.zone
.toString();
2114 doc
.push_back(object
);
2121 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2122 if(req
->method
!= "PUT")
2123 throw HttpMethodNotAllowedException();
2125 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2127 uint64_t count
= purgeAuthCachesExact(canon
);
2128 resp
->setBody(Json::object
{
2129 { "count", (int) count
},
2130 { "result", "Flushed cache." }
2134 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2136 resp
->headers
["Cache-Control"] = "max-age=86400";
2137 resp
->headers
["Content-Type"] = "text/css";
2140 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2141 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2142 ret
<<"a { color: #0959c2; }"<<endl
;
2143 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2144 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2145 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2146 ret
<<".row:after { clear: both; }"<<endl
;
2147 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2148 ret
<<".all { width: 100%; }"<<endl
;
2149 ret
<<".headl { width: 60%; }"<<endl
;
2150 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2151 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=);";
2152 ret
<<" width: 154px; height: 20px; }"<<endl
;
2153 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2154 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2155 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2156 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2157 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2158 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2159 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2160 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2161 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2162 ret
<<".resetring {float: right; }"<<endl
;
2163 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
;
2164 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2165 ret
<<".resizering {float: right;}"<<endl
;
2166 resp
->body
= ret
.str();
2170 void AuthWebServer::webThread()
2173 setThreadName("pdns/webserver");
2174 if(::arg().mustDo("api")) {
2175 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2176 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2177 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2178 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2179 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2180 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2181 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2182 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2183 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2184 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2185 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2186 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2187 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2188 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2189 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2190 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2191 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2192 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2193 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2195 if (::arg().mustDo("webserver")) {
2196 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2197 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2202 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;