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 // 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
};
64 AuthWebServer::AuthWebServer() :
66 d_start(time(nullptr)),
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"]);
77 acl
.toMasks(::arg()["webserver-allow-from"]);
84 void AuthWebServer::go()
87 pthread_create(&d_tid
, 0, webThreadHelper
, this);
88 pthread_create(&d_tid
, 0, statThreadHelper
, this);
91 void AuthWebServer::statThread()
94 setThreadName("pdns/statHelper");
96 d_queries
.submit(S
.read("udp-queries"));
97 d_cachehits
.submit(S
.read("packetcache-hit"));
98 d_cachemisses
.submit(S
.read("packetcache-miss"));
99 d_qcachehits
.submit(S
.read("query-cache-hit"));
100 d_qcachemisses
.submit(S
.read("query-cache-miss"));
105 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
110 void *AuthWebServer::statThreadHelper(void *p
)
112 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
114 return 0; // never reached
117 void *AuthWebServer::webThreadHelper(void *p
)
119 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
121 return 0; // never reached
124 static string
htmlescape(const string
&s
) {
126 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
147 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
151 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
153 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
158 ret
<<"<div class=\"panel\">";
159 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
160 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
161 ret
<<"<div class=ringmeta>";
162 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
163 ret
<<"<span class=resizering>Resize: ";
164 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
165 for(int i
=0;sizes
[i
];++i
) {
166 if(S
.getRingSize(ringname
)!=sizes
[i
])
167 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
169 ret
<<"("<<sizes
[i
]<<") ";
171 ret
<<"</span></div>";
173 ret
<<"<table class=\"data\">";
175 int total
=max(1,tot
);
176 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
177 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
180 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
182 ret
<<"<tr><td><b>Rest:</b></td><td><b>"<<tot
-printed
<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot
-printed
)*100.0/total
)<<"</b></td>"<<endl
;
184 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
185 ret
<<"</table></div>"<<endl
;
188 void AuthWebServer::printvars(ostringstream
&ret
)
190 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
192 vector
<string
>entries
=S
.getEntries();
193 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
194 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
197 ret
<<"</table></div>"<<endl
;
200 void AuthWebServer::printargs(ostringstream
&ret
)
202 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
204 vector
<string
>entries
=arg().list();
205 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
206 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
210 string
AuthWebServer::makePercentage(const double& val
)
212 return (boost::format("%.01f%%") % val
).str();
215 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
217 if(!req
->getvars
["resetring"].empty()) {
218 if (S
.ringExists(req
->getvars
["resetring"]))
219 S
.resetRing(req
->getvars
["resetring"]);
221 resp
->headers
["Location"] = req
->url
.path
;
224 if(!req
->getvars
["resizering"].empty()){
225 int size
=std::stoi(req
->getvars
["size"]);
226 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
227 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
229 resp
->headers
["Location"] = req
->url
.path
;
235 ret
<<"<!DOCTYPE html>"<<endl
;
236 ret
<<"<html><head>"<<endl
;
237 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
238 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
239 ret
<<"</head><body>"<<endl
;
241 ret
<<"<div class=\"row\">"<<endl
;
242 ret
<<"<div class=\"headl columns\">";
243 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
244 if(!arg()["config-name"].empty()) {
245 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
247 ret
<<"</a></div>"<<endl
;
248 ret
<<"<div class=\"headr columns\"></div></div>";
249 ret
<<"<div class=\"row\"><div class=\"all columns\">";
251 time_t passed
=time(0)-s_starttime
;
254 humanDuration(passed
)<<
257 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
258 (int)d_queries
.get1()<<", "<<
259 (int)d_queries
.get5()<<", "<<
260 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
263 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
264 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
265 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
266 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
267 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
270 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
271 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
272 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
273 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
274 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
277 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
278 (int)d_qcachemisses
.get1()<<", "<<
279 (int)d_qcachemisses
.get5()<<", "<<
280 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
283 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
284 if(req
->getvars
["ring"].empty()) {
285 auto entries
= S
.listRings();
286 for(const auto &i
: entries
) {
287 printtable(ret
, i
, S
.getRingTitle(i
));
291 if(arg().mustDo("webserver-print-arguments"))
294 else if(S
.ringExists(req
->getvars
["ring"]))
295 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
297 ret
<<"</div></div>"<<endl
;
298 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
299 ret
<<"</body></html>"<<endl
;
301 resp
->body
= ret
.str();
305 /** Helper to build a record content as needed. */
306 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
307 // noDot: for backend storage, pass true. for API users, pass false.
308 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
309 return drc
->getZoneRepresentation(noDot
);
312 /** "Normalize" record content for API consumers. */
313 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
314 return makeRecordContent(qtype
, content
, false);
317 /** "Normalize" record content for backend storage. */
318 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
319 return makeRecordContent(qtype
, content
, true);
322 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
*dk
) {
323 string zoneId
= apiZoneNameToId(di
.zone
);
324 vector
<string
> masters
;
325 for(const auto& m
: di
.masters
)
326 masters
.push_back(m
.toStringWithPortExcept(53));
328 return Json::object
{
329 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
331 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
332 { "name", di
.zone
.toString() },
333 { "kind", di
.getKindString() },
334 { "dnssec", dk
->isSecuredZone(di
.zone
) },
335 { "account", di
.account
},
336 { "masters", masters
},
337 { "serial", (double)di
.serial
},
338 { "notified_serial", (double)di
.notified_serial
},
339 { "last_check", (double)di
.last_check
}
343 static bool shouldDoRRSets(HttpRequest
* req
) {
344 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
346 if (req
->getvars
["rrsets"] == "false")
348 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
351 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
354 if(!B
.getDomainInfo(zonename
, di
)) {
355 throw HttpNotFoundException();
359 Json::object doc
= getZoneInfo(di
, &dk
);
360 // extra stuff getZoneInfo doesn't do for us (more expensive)
362 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
363 doc
["soa_edit_api"] = soa_edit_api
;
365 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
366 doc
["soa_edit"] = soa_edit
;
368 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
369 doc
["nsec3param"] = nsec3param
;
371 bool nsec3narrowbool
= false;
372 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
373 if (nsec3narrow
== "1")
374 nsec3narrowbool
= true;
375 doc
["nsec3narrow"] = nsec3narrowbool
;
378 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
379 doc
["api_rectify"] = (api_rectify
== "1");
382 vector
<string
> tsig_master
, tsig_slave
;
383 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
384 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
386 Json::array tsig_master_keys
;
387 for (const auto& keyname
: tsig_master
) {
388 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
390 doc
["master_tsig_key_ids"] = tsig_master_keys
;
392 Json::array tsig_slave_keys
;
393 for (const auto& keyname
: tsig_slave
) {
394 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
396 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
399 vector
<DNSResourceRecord
> records
;
400 vector
<Comment
> comments
;
402 // load all records + sort
404 DNSResourceRecord rr
;
405 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
406 while(di
.backend
->get(rr
)) {
407 if (!rr
.qtype
.getCode())
408 continue; // skip empty non-terminals
409 records
.push_back(rr
);
411 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
412 /* if you ever want to update this comparison function,
413 please be aware that you will also need to update the conditions in the code merging
414 the records and comments below */
415 if (a
.qname
== b
.qname
) {
416 return b
.qtype
< a
.qtype
;
418 return b
.qname
< a
.qname
;
422 // load all comments + sort
425 di
.backend
->listComments(di
.id
);
426 while(di
.backend
->getComment(comment
)) {
427 comments
.push_back(comment
);
429 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
430 /* if you ever want to update this comparison function,
431 please be aware that you will also need to update the conditions in the code merging
432 the records and comments below */
433 if (a
.qname
== b
.qname
) {
434 return b
.qtype
< a
.qtype
;
436 return b
.qname
< a
.qname
;
442 Json::array rrset_records
;
443 Json::array rrset_comments
;
444 DNSName current_qname
;
447 auto rit
= records
.begin();
448 auto cit
= comments
.begin();
450 while (rit
!= records
.end() || cit
!= comments
.end()) {
451 // 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
452 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
453 current_qname
= rit
->qname
;
454 current_qtype
= rit
->qtype
;
457 current_qname
= cit
->qname
;
458 current_qtype
= cit
->qtype
;
462 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
463 ttl
= min(ttl
, rit
->ttl
);
464 rrset_records
.push_back(Json::object
{
465 { "disabled", rit
->disabled
},
466 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
470 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
471 rrset_comments
.push_back(Json::object
{
472 { "modified_at", (double)cit
->modified_at
},
473 { "account", cit
->account
},
474 { "content", cit
->content
}
479 rrset
["name"] = current_qname
.toString();
480 rrset
["type"] = current_qtype
.getName();
481 rrset
["records"] = rrset_records
;
482 rrset
["comments"] = rrset_comments
;
483 rrset
["ttl"] = (double)ttl
;
484 rrsets
.push_back(rrset
);
486 rrset_records
.clear();
487 rrset_comments
.clear();
490 doc
["rrsets"] = rrsets
;
496 void productServerStatisticsFetch(map
<string
,string
>& out
)
498 vector
<string
> items
= S
.getEntries();
499 for(const string
& item
: items
) {
500 out
[item
] = std::to_string(S
.read(item
));
504 out
["uptime"] = std::to_string(time(0) - s_starttime
);
507 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
510 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
518 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
519 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
520 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
524 static void gatherRecords(const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
526 DNSResourceRecord rr
;
532 validateGatheredRRType(rr
);
533 const auto& items
= container
["records"].array_items();
534 for(const auto& record
: items
) {
535 string content
= stringFromJson(record
, "content");
536 rr
.disabled
= boolFromJson(record
, "disabled");
538 // validate that the client sent something we can actually parse, and require that data to be dotted.
540 if (rr
.qtype
.getCode() != QType::AAAA
) {
541 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
542 if (!pdns_iequals(tmp
, content
)) {
543 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
546 struct in6_addr tmpbuf
;
547 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
548 throw std::runtime_error("Invalid IPv6 address");
551 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
553 catch(std::exception
& e
)
555 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
558 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
559 boolFromJson(record
, "set-ptr", false) == true) {
560 DNSResourceRecord ptr
;
563 // verify that there's a zone for the PTR
565 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
566 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
568 ptr
.domain_id
= sd
.domain_id
;
569 new_ptrs
.push_back(ptr
);
572 new_records
.push_back(rr
);
576 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
581 time_t now
= time(0);
582 for (auto comment
: container
["comments"].array_items()) {
583 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
584 c
.content
= stringFromJson(comment
, "content");
585 c
.account
= stringFromJson(comment
, "account");
586 new_comments
.push_back(c
);
590 static void checkDefaultDNSSECAlgos() {
591 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
592 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
593 int k_size
= arg().asNum("default-ksk-size");
594 int z_size
= arg().asNum("default-zsk-size");
596 // Sanity check DNSSEC parameters
597 if (::arg()["default-zsk-algorithm"] != "") {
599 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
600 else if (k_algo
<= 10 && k_size
== 0)
601 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
604 if (::arg()["default-zsk-algorithm"] != "") {
606 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
607 else if (z_algo
<= 10 && z_size
== 0)
608 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
612 static void throwUnableToSecure(const DNSName
& zonename
) {
613 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
614 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
617 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
) {
618 vector
<string
> zonemaster
;
619 bool shouldRectify
= false;
620 for(auto value
: document
["masters"].array_items()) {
621 string master
= value
.string_value();
623 throw ApiException("Master can not be an empty string");
624 zonemaster
.push_back(master
);
627 if (zonemaster
.size()) {
628 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
630 if (document
["kind"].is_string()) {
631 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
633 if (document
["soa_edit_api"].is_string()) {
634 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
636 if (document
["soa_edit"].is_string()) {
637 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
640 bool api_rectify
= boolFromJson(document
, "api_rectify");
641 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
643 catch (const JsonException
&) {}
645 if (document
["account"].is_string()) {
646 di
.backend
->setAccount(zonename
, document
["account"].string_value());
650 bool dnssecInJSON
= false;
651 bool dnssecDocVal
= false;
654 dnssecDocVal
= boolFromJson(document
, "dnssec");
657 catch (const JsonException
&) {}
659 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
664 checkDefaultDNSSECAlgos();
666 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
667 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
668 int k_size
= arg().asNum("default-ksk-size");
669 int z_size
= arg().asNum("default-zsk-size");
673 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
674 throwUnableToSecure(zonename
);
680 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
681 throwUnableToSecure(zonename
);
685 // Used later for NSEC3PARAM
686 isDNSSECZone
= dk
.isSecuredZone(zonename
);
689 throwUnableToSecure(zonename
);
691 shouldRectify
= true;
694 // "dnssec": false in json
697 if (!dk
.unSecureZone(zonename
, error
, info
)) {
698 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
700 isDNSSECZone
= dk
.isSecuredZone(zonename
);
702 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
704 shouldRectify
= true;
709 if(document
["nsec3param"].string_value().length() > 0) {
710 shouldRectify
= true;
711 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
712 string error_msg
= "";
714 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
716 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
717 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
719 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
720 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
721 "' passed our basic sanity checks, but cannot be used with the current backend.");
725 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
728 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
729 if (api_rectify
.empty()) {
730 if (::arg().mustDo("default-api-rectify")) {
734 if (api_rectify
== "1") {
737 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true)) {
738 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
743 string soa_edit_api_kind
;
744 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
745 if (!soa_edit_api_kind
.empty()) {
747 if (!B
.getSOAUncached(zonename
, sd
))
750 string soa_edit_kind
;
751 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
753 DNSResourceRecord rr
;
754 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
755 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
756 throw ApiException("Hosting backend does not support editing records.");
762 if (!document
["master_tsig_key_ids"].is_null()) {
763 vector
<string
> metadata
;
766 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
767 auto keyname(apiZoneIdToName(value
.string_value()));
768 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
769 if (keyAlgo
.empty() || keyContent
.empty()) {
770 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
772 metadata
.push_back(keyname
.toString());
774 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
775 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
778 if (!document
["slave_tsig_key_ids"].is_null()) {
779 vector
<string
> metadata
;
782 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
783 auto keyname(apiZoneIdToName(value
.string_value()));
784 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
785 if (keyAlgo
.empty() || keyContent
.empty()) {
786 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
788 metadata
.push_back(keyname
.toString());
790 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
791 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
796 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
797 static vector
<string
> builtinOptions
{
800 "ALLOW-DNSUPDATE-FROM",
801 "TSIG-ALLOW-DNSUPDATE",
803 "SOA-EDIT-DNSUPDATE",
807 "GSS-ALLOW-AXFR-PRINCIPAL",
808 "GSS-ACCEPTOR-PRINCIPAL",
818 "TSIG-ALLOW-DNSUPDATE"
821 // the following options do not allow modifications via API
822 static vector
<string
> protectedOptions
{
832 if (kind
.find("X-") == 0)
837 for (const string
& s
: builtinOptions
) {
839 for (const string
& s2
: protectedOptions
) {
840 if (!readonly
&& s
== s2
)
851 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
852 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
856 if (!B
.getDomainInfo(zonename
, di
)) {
857 throw HttpNotFoundException();
860 if (req
->method
== "GET") {
861 map
<string
, vector
<string
> > md
;
862 Json::array document
;
864 if (!B
.getAllDomainMetadata(zonename
, md
))
865 throw HttpNotFoundException();
867 for (const auto& i
: md
) {
869 for (string j
: i
.second
)
870 entries
.push_back(j
);
873 { "type", "Metadata" },
875 { "metadata", entries
}
878 document
.push_back(key
);
881 resp
->setBody(document
);
882 } else if (req
->method
== "POST") {
883 auto document
= req
->json();
885 vector
<string
> entries
;
888 kind
= stringFromJson(document
, "kind");
889 } catch (const JsonException
&) {
890 throw ApiException("kind is not specified or not a string");
893 if (!isValidMetadataKind(kind
, false))
894 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
896 vector
<string
> vecMetadata
;
898 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
899 throw ApiException("Could not retrieve metadata entries for domain '" +
900 zonename
.toString() + "'");
902 auto& metadata
= document
["metadata"];
903 if (!metadata
.is_array())
904 throw ApiException("metadata is not specified or not an array");
906 for (const auto& i
: metadata
.array_items()) {
908 throw ApiException("metadata must be strings");
909 else if (std::find(vecMetadata
.cbegin(),
911 i
.string_value()) == vecMetadata
.cend()) {
912 vecMetadata
.push_back(i
.string_value());
916 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
917 throw ApiException("Could not update metadata entries for domain '" +
918 zonename
.toString() + "'");
920 Json::array respMetadata
;
921 for (const string
& s
: vecMetadata
)
922 respMetadata
.push_back(s
);
925 { "type", "Metadata" },
926 { "kind", document
["kind"] },
927 { "metadata", respMetadata
}
933 throw HttpMethodNotAllowedException();
936 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
937 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
941 if (!B
.getDomainInfo(zonename
, di
)) {
942 throw HttpNotFoundException();
945 string kind
= req
->parameters
["kind"];
947 if (req
->method
== "GET") {
948 vector
<string
> metadata
;
949 Json::object document
;
952 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
953 throw HttpNotFoundException();
954 else if (!isValidMetadataKind(kind
, true))
955 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
957 document
["type"] = "Metadata";
958 document
["kind"] = kind
;
960 for (const string
& i
: metadata
)
961 entries
.push_back(i
);
963 document
["metadata"] = entries
;
964 resp
->setBody(document
);
965 } else if (req
->method
== "PUT") {
966 auto document
= req
->json();
968 if (!isValidMetadataKind(kind
, false))
969 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
971 vector
<string
> vecMetadata
;
972 auto& metadata
= document
["metadata"];
973 if (!metadata
.is_array())
974 throw ApiException("metadata is not specified or not an array");
976 for (const auto& i
: metadata
.array_items()) {
978 throw ApiException("metadata must be strings");
979 vecMetadata
.push_back(i
.string_value());
982 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
983 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
986 { "type", "Metadata" },
988 { "metadata", metadata
}
992 } else if (req
->method
== "DELETE") {
993 if (!isValidMetadataKind(kind
, false))
994 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
996 vector
<string
> md
; // an empty vector will do it
997 if (!B
.setDomainMetadata(zonename
, kind
, md
))
998 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1000 throw HttpMethodNotAllowedException();
1003 // Throws 404 if the key with inquireKeyId does not exist
1004 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1005 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1007 for(const auto& value
: keyset
) {
1008 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1014 throw HttpNotFoundException();
1018 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1019 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1021 bool inquireSingleKey
= inquireKeyId
>= 0;
1024 for(const auto& value
: keyset
) {
1025 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1030 switch (value
.second
.keyType
) {
1031 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1032 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1033 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1037 { "type", "Cryptokey" },
1038 { "id", (int)value
.second
.id
},
1039 { "active", value
.second
.active
},
1040 { "keytype", keyType
},
1041 { "flags", (uint16_t)value
.first
.d_flags
},
1042 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1043 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1044 { "bits", value
.first
.getKey()->getBits() }
1047 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1049 for(const uint8_t keyid
: { DNSSECKeeper::SHA1
, DNSSECKeeper::SHA256
, DNSSECKeeper::GOST
, DNSSECKeeper::SHA384
})
1051 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1056 if (inquireSingleKey
) {
1057 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1064 if (inquireSingleKey
) {
1065 // we came here because we couldn't find the requested key.
1066 throw HttpNotFoundException();
1073 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1074 * It deletes a key from :zone_name specified by :cryptokey_id.
1076 * Case 1: the backend returns true on removal. This means the key is gone.
1077 * The server returns 204 No Content, no body.
1078 * Case 2: the backend returns false on removal. An error occurred.
1079 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1080 * Case 3: the key or zone does not exist.
1081 * The server returns 404 Not Found
1083 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1084 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1088 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1093 * This method adds a key to a zone by generate it or content parameter.
1096 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1097 * "keytype" : "ksk|zsk" <string>
1098 * "active" : "true|false" <value>
1099 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1100 * "bits" : number of bits <int>
1104 * Case 1: keytype isn't ksk|zsk
1105 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1106 * Case 2: 'bits' must be a positive integer value.
1107 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1108 * Case 3: The "algorithm" isn't supported
1109 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1110 * Case 4: Algorithm <= 10 and no bits were passed
1111 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1112 * Case 5: The wrong keysize was passed
1113 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1114 * Case 6: If the server cant guess the keysize
1115 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1116 * Case 7: The key-creation failed
1117 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1118 * Case 8: The key in content has the wrong format
1119 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1120 * Case 9: The wrong combination of fields is submitted
1121 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1122 * Case 10: No content and everything was fine
1123 * The server returns 201 Created and all public data about the new cryptokey
1124 * Case 11: With specified content
1125 * The server returns 201 Created and all public data about the added cryptokey
1128 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1129 auto document
= req
->json();
1130 string privatekey_fieldname
= "privatekey";
1131 auto privatekey
= document
["privatekey"];
1132 if (privatekey
.is_null()) {
1133 // Fallback to the old "content" behaviour
1134 privatekey
= document
["content"];
1135 privatekey_fieldname
= "content";
1137 bool active
= boolFromJson(document
, "active", false);
1140 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1142 } else if (stringFromJson(document
, "keytype") == "zsk") {
1145 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1148 int64_t insertedId
= -1;
1150 if (privatekey
.is_null()) {
1151 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1152 auto docbits
= document
["bits"];
1153 if (!docbits
.is_null()) {
1154 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1155 throw ApiException("'bits' must be a positive integer value");
1157 bits
= docbits
.int_value();
1160 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1161 auto providedAlgo
= document
["algorithm"];
1162 if (providedAlgo
.is_string()) {
1163 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1164 if (algorithm
== -1)
1165 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1166 } else if (providedAlgo
.is_number()) {
1167 algorithm
= providedAlgo
.int_value();
1168 } else if (!providedAlgo
.is_null()) {
1169 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1173 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
)) {
1174 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1176 } catch (std::runtime_error
& error
) {
1177 throw ApiException(error
.what());
1180 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1181 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1182 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1183 DNSKEYRecordContent dkrc
;
1184 DNSSECPrivateKey dpk
;
1186 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1187 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1188 // TODO remove in 4.2.0
1189 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1190 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1199 catch (std::runtime_error
& error
) {
1200 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1202 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
)) {
1203 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1205 } catch (std::runtime_error
& error
) {
1206 throw ApiException(error
.what());
1209 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1211 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1213 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1218 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1219 * It de/activates a key from :zone_name specified by :cryptokey_id.
1221 * Case 1: invalid JSON data
1222 * The server returns 400 Bad Request
1223 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1224 * The server returns 204 No Content
1225 * Case 3: the backend returns false on de/activation. An error occurred.
1226 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1228 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1229 //throws an exception if the Body is empty
1230 auto document
= req
->json();
1231 //throws an exception if the key does not exist or is not a bool
1232 bool active
= boolFromJson(document
, "active");
1234 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1235 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1239 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1240 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1250 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1251 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1252 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1254 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1255 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1258 DNSSECKeeper
dk(&B
);
1260 if (!B
.getDomainInfo(zonename
, di
)) {
1261 throw HttpNotFoundException();
1264 int inquireKeyId
= -1;
1265 if (req
->parameters
.count("key_id")) {
1266 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1267 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1270 if (req
->method
== "GET") {
1271 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1272 } else if (req
->method
== "DELETE") {
1273 if (inquireKeyId
== -1)
1274 throw HttpBadRequestException();
1275 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1276 } else if (req
->method
== "POST") {
1277 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1278 } else if (req
->method
== "PUT") {
1279 if (inquireKeyId
== -1)
1280 throw HttpBadRequestException();
1281 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1283 throw HttpMethodNotAllowedException(); //Returns method not allowed
1287 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1288 DNSResourceRecord rr
;
1289 vector
<string
> zonedata
;
1290 stringtok(zonedata
, zonestring
, "\r\n");
1292 ZoneParserTNG
zpt(zonedata
, zonename
);
1296 string comment
= "Imported via the API";
1299 while(zpt
.get(rr
, &comment
)) {
1300 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1302 if(rr
.qtype
.getCode() == QType::SOA
)
1304 validateGatheredRRType(rr
);
1306 new_records
.push_back(rr
);
1309 catch(std::exception
& ae
) {
1310 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1314 /** Throws ApiException if records which violate RRset contraints are present.
1315 * NOTE: sorts records in-place.
1317 * Constraints being checked:
1318 * *) no exact duplicates
1319 * *) no duplicates for QTypes that can only be present once per RRset
1320 * *) hostnames are hostnames
1322 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1323 sort(records
.begin(), records
.end(),
1324 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1325 /* we need _strict_ weak ordering */
1326 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1330 DNSResourceRecord previous
;
1331 for(const auto& rec
: records
) {
1332 if (previous
.qname
== rec
.qname
) {
1333 if (previous
.qtype
== rec
.qtype
) {
1334 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1335 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1337 if (previous
.content
== rec
.content
) {
1338 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1340 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1341 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1345 // Check if the DNSNames that should be hostnames, are hostnames
1347 checkHostnameCorrectness(rec
);
1348 } catch (const std::exception
& e
) {
1349 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1356 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1358 string contentFromDB
;
1359 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1360 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1361 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1365 if (!getTSIGHashEnum(algo
, the
)) {
1366 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1370 if (B64Decode(content
, b64out
) == -1) {
1371 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1375 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1376 Json::object tsigkey
= {
1377 { "name", keyname
.toStringNoDot() },
1378 { "id", apiZoneNameToId(keyname
) },
1379 { "algorithm", algo
.toStringNoDot() },
1381 { "type", "TSIGKey" }
1386 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1387 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1390 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1392 if (req
->method
== "GET") {
1393 vector
<struct TSIGKey
> keys
;
1395 if (!B
.getTSIGKeys(keys
)) {
1396 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1401 for(const auto &key
: keys
) {
1402 doc
.push_back(makeJSONTSIGKey(key
, false));
1405 } else if (req
->method
== "POST") {
1406 auto document
= req
->json();
1407 DNSName
keyname(stringFromJson(document
, "name"));
1408 DNSName
algo(stringFromJson(document
, "algorithm"));
1409 string content
= document
["key"].string_value();
1411 if (content
.empty()) {
1413 content
= makeTSIGKey(algo
);
1414 } catch (const PDNSException
& e
) {
1415 throw HttpBadRequestException(e
.reason
);
1419 // Will throw an ApiException or HttpConflictException on error
1420 checkTSIGKey(B
, keyname
, algo
, content
);
1422 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1423 throw HttpInternalServerErrorException("Unable to add TSIG key");
1427 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1429 throw HttpMethodNotAllowedException();
1433 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1435 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1439 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1440 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1445 tsk
.algorithm
= algo
;
1448 if (req
->method
== "GET") {
1449 resp
->setBody(makeJSONTSIGKey(tsk
));
1450 } else if (req
->method
== "PUT") {
1451 json11::Json document
;
1452 if (!req
->body
.empty()) {
1453 document
= req
->json();
1455 if (document
["name"].is_string()) {
1456 tsk
.name
= DNSName(document
["name"].string_value());
1458 if (document
["algorithm"].is_string()) {
1459 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1462 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1463 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1466 if (document
["key"].is_string()) {
1467 string new_content
= document
["key"].string_value();
1469 if (B64Decode(new_content
, decoded
) == -1) {
1470 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1472 tsk
.key
= new_content
;
1474 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1475 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1477 if (tsk
.name
!= keyname
) {
1478 // Remove the old key
1479 if (!B
.deleteTSIGKey(keyname
)) {
1480 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1483 resp
->setBody(makeJSONTSIGKey(tsk
));
1484 } else if (req
->method
== "DELETE") {
1485 if (!B
.deleteTSIGKey(keyname
)) {
1486 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1492 throw HttpMethodNotAllowedException();
1496 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1498 DNSSECKeeper
dk(&B
);
1499 if (req
->method
== "POST") {
1501 auto document
= req
->json();
1502 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1503 apiCheckNameAllowedCharacters(zonename
.toString());
1504 zonename
.makeUsLowerCase();
1506 bool exists
= B
.getDomainInfo(zonename
, di
);
1508 throw HttpConflictException();
1510 // validate 'kind' is set
1511 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1513 string zonestring
= document
["zone"].string_value();
1514 auto rrsets
= document
["rrsets"];
1515 if (rrsets
.is_array() && zonestring
!= "")
1516 throw ApiException("You cannot give rrsets AND zone data as text");
1518 auto nameservers
= document
["nameservers"];
1519 if (!nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1520 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
1522 string soa_edit_api_kind
;
1523 if (document
["soa_edit_api"].is_string()) {
1524 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1527 soa_edit_api_kind
= "DEFAULT";
1529 string soa_edit_kind
= document
["soa_edit"].string_value();
1531 // if records/comments are given, load and check them
1532 bool have_soa
= false;
1533 bool have_zone_ns
= false;
1534 vector
<DNSResourceRecord
> new_records
;
1535 vector
<Comment
> new_comments
;
1536 vector
<DNSResourceRecord
> new_ptrs
;
1538 if (rrsets
.is_array()) {
1539 for (const auto& rrset
: rrsets
.array_items()) {
1540 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1541 apiCheckQNameAllowedCharacters(qname
.toString());
1543 qtype
= stringFromJson(rrset
, "type");
1544 if (qtype
.getCode() == 0) {
1545 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1547 if (rrset
["records"].is_array()) {
1548 int ttl
= intFromJson(rrset
, "ttl");
1549 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1551 if (rrset
["comments"].is_array()) {
1552 gatherComments(rrset
, qname
, qtype
, new_comments
);
1555 } else if (zonestring
!= "") {
1556 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1559 for(auto& rr
: new_records
) {
1560 rr
.qname
.makeUsLowerCase();
1561 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1562 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1563 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1565 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1567 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1569 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1570 have_zone_ns
= true;
1574 // synthesize RRs as needed
1575 DNSResourceRecord autorr
;
1576 autorr
.qname
= zonename
;
1578 autorr
.ttl
= ::arg().asNum("default-ttl");
1580 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1581 // synthesize a SOA record so the zone "really" exists
1582 string soa
= (boost::format("%s %s %ul")
1583 % ::arg()["default-soa-name"]
1584 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1585 % document
["serial"].int_value()
1588 fillSOAData(soa
, sd
); // fills out default values for us
1589 autorr
.qtype
= QType::SOA
;
1590 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1591 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1592 new_records
.push_back(autorr
);
1595 // create NS records if nameservers are given
1596 for (auto value
: nameservers
.array_items()) {
1597 string nameserver
= value
.string_value();
1598 if (nameserver
.empty())
1599 throw ApiException("Nameservers must be non-empty strings");
1600 if (!isCanonical(nameserver
))
1601 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1603 // ensure the name parses
1604 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1606 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1608 autorr
.qtype
= QType::NS
;
1609 new_records
.push_back(autorr
);
1611 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1615 checkNewRecords(new_records
);
1617 if (boolFromJson(document
, "dnssec", false)) {
1618 checkDefaultDNSSECAlgos();
1620 if(document
["nsec3param"].string_value().length() > 0) {
1621 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1622 string error_msg
= "";
1623 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1624 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1629 // no going back after this
1630 if(!B
.createDomain(zonename
))
1631 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1633 if(!B
.getDomainInfo(zonename
, di
))
1634 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1636 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1637 if (!soa_edit_api_kind
.empty()) {
1638 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1641 di
.backend
->startTransaction(zonename
, di
.id
);
1643 for(auto rr
: new_records
) {
1644 rr
.domain_id
= di
.id
;
1645 di
.backend
->feedRecord(rr
, DNSName());
1647 for(Comment
& c
: new_comments
) {
1648 c
.domain_id
= di
.id
;
1649 di
.backend
->feedComment(c
);
1652 updateDomainSettingsFromDocument(B
, di
, zonename
, document
);
1654 di
.backend
->commitTransaction();
1656 storeChangedPTRs(B
, new_ptrs
);
1658 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1663 if(req
->method
!= "GET")
1664 throw HttpMethodNotAllowedException();
1666 vector
<DomainInfo
> domains
;
1668 if (req
->getvars
.count("zone")) {
1669 string zone
= req
->getvars
["zone"];
1670 apiCheckNameAllowedCharacters(zone
);
1671 DNSName zonename
= apiNameToDNSName(zone
);
1672 zonename
.makeUsLowerCase();
1674 if (B
.getDomainInfo(zonename
, di
)) {
1675 domains
.push_back(di
);
1678 B
.getAllDomains(&domains
, true); // incl. disabled
1682 for(const DomainInfo
& di
: domains
) {
1683 doc
.push_back(getZoneInfo(di
, &dk
));
1688 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1689 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1693 if (!B
.getDomainInfo(zonename
, di
)) {
1694 throw HttpNotFoundException();
1697 if(req
->method
== "PUT") {
1698 // update domain settings
1700 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json());
1703 resp
->status
= 204; // No Content, but indicate success
1706 else if(req
->method
== "DELETE") {
1708 if(!di
.backend
->deleteDomain(zonename
))
1709 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1712 DNSSECKeeper
dk(&B
);
1713 dk
.clearCaches(zonename
);
1714 purgeAuthCaches(zonename
.toString() + "$");
1716 // empty body on success
1718 resp
->status
= 204; // No Content: declare that the zone is gone now
1720 } else if (req
->method
== "PATCH") {
1721 patchZone(req
, resp
);
1723 } else if (req
->method
== "GET") {
1724 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1727 throw HttpMethodNotAllowedException();
1730 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1731 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1733 if(req
->method
!= "GET")
1734 throw HttpMethodNotAllowedException();
1740 if (!B
.getDomainInfo(zonename
, di
)) {
1741 throw HttpNotFoundException();
1744 DNSResourceRecord rr
;
1746 di
.backend
->list(zonename
, di
.id
);
1747 while(di
.backend
->get(rr
)) {
1748 if (!rr
.qtype
.getCode())
1749 continue; // skip empty non-terminals
1752 rr
.qname
.toString() << "\t" <<
1755 rr
.qtype
.getName() << "\t" <<
1756 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1760 if (req
->accept_json
) {
1761 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1763 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1764 resp
->body
= ss
.str();
1768 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1769 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1771 if(req
->method
!= "PUT")
1772 throw HttpMethodNotAllowedException();
1776 if (!B
.getDomainInfo(zonename
, di
)) {
1777 throw HttpNotFoundException();
1780 if(di
.masters
.empty())
1781 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1783 random_shuffle(di
.masters
.begin(), di
.masters
.end());
1784 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1785 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1788 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1789 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1791 if(req
->method
!= "PUT")
1792 throw HttpMethodNotAllowedException();
1796 if (!B
.getDomainInfo(zonename
, di
)) {
1797 throw HttpNotFoundException();
1800 if(!Communicator
.notifyDomain(zonename
))
1801 throw ApiException("Failed to add to the queue - see server log");
1803 resp
->setSuccessResult("Notification queued");
1806 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1807 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1809 if(req
->method
!= "PUT")
1810 throw HttpMethodNotAllowedException();
1814 if (!B
.getDomainInfo(zonename
, di
)) {
1815 throw HttpNotFoundException();
1818 DNSSECKeeper
dk(&B
);
1820 if (!dk
.isSecuredZone(zonename
))
1821 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1823 if (di
.kind
== DomainInfo::Slave
)
1824 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1826 string error_msg
= "";
1828 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1829 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1831 resp
->setSuccessResult("Rectified");
1834 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1835 if (rr
.qtype
.getCode() == QType::A
) {
1837 if (!IpToU32(rr
.content
, &ip
)) {
1838 throw ApiException("PTR: Invalid IP address given");
1840 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1841 % ((ip
>> 24) & 0xff)
1842 % ((ip
>> 16) & 0xff)
1843 % ((ip
>> 8) & 0xff)
1846 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1847 ComboAddress
ca(rr
.content
);
1850 for (int octet
= 0; octet
< 16; ++octet
) {
1851 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1852 // this should be impossible: no byte should give more than two digits in hex format
1853 throw PDNSException("Formatting IPv6 address failed");
1855 ss
<< buf
[0] << '.' << buf
[1] << '.';
1857 string tmp
= ss
.str();
1858 tmp
.resize(tmp
.size()-1); // remove last dot
1859 // reverse and append arpa domain
1860 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1862 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1867 ptr
->disabled
= rr
.disabled
;
1868 ptr
->content
= rr
.qname
.toStringRootDot();
1871 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1872 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1874 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1875 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1877 string soa_edit_api_kind
;
1878 string soa_edit_kind
;
1879 bool soa_changed
= false;
1880 DNSResourceRecord soarr
;
1881 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1882 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1883 if (!soa_edit_api_kind
.empty()) {
1884 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1887 sd
.db
->startTransaction(sd
.qname
);
1888 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1889 sd
.db
->abortTransaction();
1890 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1894 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1897 sd
.db
->commitTransaction();
1898 purgeAuthCachesExact(rr
.qname
);
1902 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
) {
1905 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1906 if (!B
.getDomainInfo(zonename
, di
)) {
1907 throw HttpNotFoundException();
1910 vector
<DNSResourceRecord
> new_records
;
1911 vector
<Comment
> new_comments
;
1912 vector
<DNSResourceRecord
> new_ptrs
;
1914 Json document
= req
->json();
1916 auto rrsets
= document
["rrsets"];
1917 if (!rrsets
.is_array())
1918 throw ApiException("No rrsets given in update request");
1920 di
.backend
->startTransaction(zonename
);
1923 string soa_edit_api_kind
;
1924 string soa_edit_kind
;
1925 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1926 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1927 bool soa_edit_done
= false;
1929 set
<pair
<DNSName
, QType
>> seen
;
1931 for (const auto& rrset
: rrsets
.array_items()) {
1932 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1933 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1934 apiCheckQNameAllowedCharacters(qname
.toString());
1936 qtype
= stringFromJson(rrset
, "type");
1937 if (qtype
.getCode() == 0) {
1938 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1941 if(seen
.count({qname
, qtype
}))
1943 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1945 seen
.insert({qname
, qtype
});
1947 if (changetype
== "DELETE") {
1948 // delete all matching qname/qtype RRs (and, implicitly comments).
1949 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1950 throw ApiException("Hosting backend does not support editing records.");
1953 else if (changetype
== "REPLACE") {
1954 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1955 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
1956 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
1958 bool replace_records
= rrset
["records"].is_array();
1959 bool replace_comments
= rrset
["comments"].is_array();
1961 if (!replace_records
&& !replace_comments
) {
1962 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
1965 new_records
.clear();
1966 new_comments
.clear();
1968 if (replace_records
) {
1969 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1970 int ttl
= intFromJson(rrset
, "ttl");
1971 // new_ptrs is merged.
1972 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1974 for(DNSResourceRecord
& rr
: new_records
) {
1975 rr
.domain_id
= di
.id
;
1976 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1977 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1980 checkNewRecords(new_records
);
1983 if (replace_comments
) {
1984 gatherComments(rrset
, qname
, qtype
, new_comments
);
1986 for(Comment
& c
: new_comments
) {
1987 c
.domain_id
= di
.id
;
1991 if (replace_records
) {
1992 bool ent_present
= false;
1993 di
.backend
->lookup(QType(QType::ANY
), qname
);
1994 DNSResourceRecord rr
;
1995 while (di
.backend
->get(rr
)) {
1996 if (qtype
.getCode() == 0) {
1999 if (qtype
.getCode() != rr
.qtype
.getCode()
2000 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2001 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2002 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2006 if (!new_records
.empty() && ent_present
) {
2008 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2009 throw ApiException("Hosting backend does not support editing records.");
2012 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2013 throw ApiException("Hosting backend does not support editing records.");
2016 if (replace_comments
) {
2017 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2018 throw ApiException("Hosting backend does not support editing comments.");
2023 throw ApiException("Changetype not understood");
2026 // edit SOA (if needed)
2027 if (!soa_edit_api_kind
.empty() && !soa_edit_done
) {
2029 if (!B
.getSOAUncached(zonename
, sd
))
2030 throw ApiException("No SOA found for domain '"+zonename
.toString()+"'");
2032 DNSResourceRecord rr
;
2033 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2034 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2035 throw ApiException("Hosting backend does not support editing records.");
2039 // return old and new serials in headers
2040 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2041 fillSOAData(rr
.content
, sd
);
2042 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2046 di
.backend
->abortTransaction();
2050 DNSSECKeeper
dk(&B
);
2052 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
2053 if (dk
.isSecuredZone(zonename
) && !dk
.isPresigned(zonename
) && api_rectify
== "1") {
2054 string error_msg
= "";
2056 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false))
2057 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2060 di
.backend
->commitTransaction();
2062 purgeAuthCachesExact(zonename
);
2065 storeChangedPTRs(B
, new_ptrs
);
2068 resp
->status
= 204; // No Content, but indicate success
2072 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2073 if(req
->method
!= "GET")
2074 throw HttpMethodNotAllowedException();
2076 string q
= req
->getvars
["q"];
2077 string sMax
= req
->getvars
["max"];
2078 string sObjectType
= req
->getvars
["object_type"];
2083 // the following types of data can be searched for using the api
2084 enum class ObjectType
2093 throw ApiException("Query q can't be blank");
2095 maxEnts
= std::stoi(sMax
);
2097 throw ApiException("Maximum entries must be larger than 0");
2099 if (sObjectType
.empty())
2100 objectType
= ObjectType::ALL
;
2101 else if (sObjectType
== "all")
2102 objectType
= ObjectType::ALL
;
2103 else if (sObjectType
== "zone")
2104 objectType
= ObjectType::ZONE
;
2105 else if (sObjectType
== "record")
2106 objectType
= ObjectType::RECORD
;
2107 else if (sObjectType
== "comment")
2108 objectType
= ObjectType::COMMENT
;
2110 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2112 SimpleMatch
sm(q
,true);
2114 vector
<DomainInfo
> domains
;
2115 vector
<DNSResourceRecord
> result_rr
;
2116 vector
<Comment
> result_c
;
2117 map
<int,DomainInfo
> zoneIdZone
;
2118 map
<int,DomainInfo
>::iterator val
;
2121 B
.getAllDomains(&domains
, true);
2123 for(const DomainInfo di
: domains
)
2125 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2126 doc
.push_back(Json::object
{
2127 { "object_type", "zone" },
2128 { "zone_id", apiZoneNameToId(di
.zone
) },
2129 { "name", di
.zone
.toString() }
2133 zoneIdZone
[di
.id
] = di
; // populate cache
2136 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2138 for(const DNSResourceRecord
& rr
: result_rr
)
2140 if (!rr
.qtype
.getCode())
2141 continue; // skip empty non-terminals
2143 auto object
= Json::object
{
2144 { "object_type", "record" },
2145 { "name", rr
.qname
.toString() },
2146 { "type", rr
.qtype
.getName() },
2147 { "ttl", (double)rr
.ttl
},
2148 { "disabled", rr
.disabled
},
2149 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2151 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2152 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2153 object
["zone"] = val
->second
.zone
.toString();
2155 doc
.push_back(object
);
2159 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2161 for(const Comment
&c
: result_c
)
2163 auto object
= Json::object
{
2164 { "object_type", "comment" },
2165 { "name", c
.qname
.toString() },
2166 { "content", c
.content
}
2168 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2169 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2170 object
["zone"] = val
->second
.zone
.toString();
2172 doc
.push_back(object
);
2179 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2180 if(req
->method
!= "PUT")
2181 throw HttpMethodNotAllowedException();
2183 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2185 uint64_t count
= purgeAuthCachesExact(canon
);
2186 resp
->setBody(Json::object
{
2187 { "count", (int) count
},
2188 { "result", "Flushed cache." }
2192 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2194 resp
->headers
["Cache-Control"] = "max-age=86400";
2195 resp
->headers
["Content-Type"] = "text/css";
2198 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2199 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2200 ret
<<"a { color: #0959c2; }"<<endl
;
2201 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2202 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2203 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2204 ret
<<".row:after { clear: both; }"<<endl
;
2205 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2206 ret
<<".all { width: 100%; }"<<endl
;
2207 ret
<<".headl { width: 60%; }"<<endl
;
2208 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2209 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=);";
2210 ret
<<" width: 154px; height: 20px; }"<<endl
;
2211 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2212 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2213 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2214 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2215 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2216 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2217 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2218 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2219 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2220 ret
<<".resetring {float: right; }"<<endl
;
2221 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
;
2222 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2223 ret
<<".resizering {float: right;}"<<endl
;
2224 resp
->body
= ret
.str();
2228 void AuthWebServer::webThread()
2231 setThreadName("pdns/webserver");
2232 if(::arg().mustDo("api")) {
2233 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2234 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2235 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2236 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2237 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2238 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2239 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2240 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2241 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2242 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2243 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2244 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2245 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2246 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2247 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2248 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2249 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2250 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2251 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2253 if (::arg().mustDo("webserver")) {
2254 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2255 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2260 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;