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"]);
75 d_ws
->setLogLevel(arg()["webserver-loglevel"]);
78 acl
.toMasks(::arg()["webserver-allow-from"]);
85 void AuthWebServer::go()
88 pthread_create(&d_tid
, 0, webThreadHelper
, this);
89 pthread_create(&d_tid
, 0, statThreadHelper
, this);
92 void AuthWebServer::statThread()
95 setThreadName("pdns/statHelper");
97 d_queries
.submit(S
.read("udp-queries"));
98 d_cachehits
.submit(S
.read("packetcache-hit"));
99 d_cachemisses
.submit(S
.read("packetcache-miss"));
100 d_qcachehits
.submit(S
.read("query-cache-hit"));
101 d_qcachemisses
.submit(S
.read("query-cache-miss"));
106 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
111 void *AuthWebServer::statThreadHelper(void *p
)
113 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
115 return 0; // never reached
118 void *AuthWebServer::webThreadHelper(void *p
)
120 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
122 return 0; // never reached
125 static string
htmlescape(const string
&s
) {
127 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
148 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
152 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
154 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
159 ret
<<"<div class=\"panel\">";
160 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
161 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
162 ret
<<"<div class=ringmeta>";
163 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
164 ret
<<"<span class=resizering>Resize: ";
165 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
166 for(int i
=0;sizes
[i
];++i
) {
167 if(S
.getRingSize(ringname
)!=sizes
[i
])
168 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
170 ret
<<"("<<sizes
[i
]<<") ";
172 ret
<<"</span></div>";
174 ret
<<"<table class=\"data\">";
176 int total
=max(1,tot
);
177 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
178 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
181 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
183 ret
<<"<tr><td><b>Rest:</b></td><td><b>"<<tot
-printed
<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot
-printed
)*100.0/total
)<<"</b></td>"<<endl
;
185 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
186 ret
<<"</table></div>"<<endl
;
189 void AuthWebServer::printvars(ostringstream
&ret
)
191 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
193 vector
<string
>entries
=S
.getEntries();
194 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
195 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
198 ret
<<"</table></div>"<<endl
;
201 void AuthWebServer::printargs(ostringstream
&ret
)
203 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
205 vector
<string
>entries
=arg().list();
206 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
207 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
211 string
AuthWebServer::makePercentage(const double& val
)
213 return (boost::format("%.01f%%") % val
).str();
216 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
218 if(!req
->getvars
["resetring"].empty()) {
219 if (S
.ringExists(req
->getvars
["resetring"]))
220 S
.resetRing(req
->getvars
["resetring"]);
222 resp
->headers
["Location"] = req
->url
.path
;
225 if(!req
->getvars
["resizering"].empty()){
226 int size
=std::stoi(req
->getvars
["size"]);
227 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
228 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
230 resp
->headers
["Location"] = req
->url
.path
;
236 ret
<<"<!DOCTYPE html>"<<endl
;
237 ret
<<"<html><head>"<<endl
;
238 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
239 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
240 ret
<<"</head><body>"<<endl
;
242 ret
<<"<div class=\"row\">"<<endl
;
243 ret
<<"<div class=\"headl columns\">";
244 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
245 if(!arg()["config-name"].empty()) {
246 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
248 ret
<<"</a></div>"<<endl
;
249 ret
<<"<div class=\"headr columns\"></div></div>";
250 ret
<<"<div class=\"row\"><div class=\"all columns\">";
252 time_t passed
=time(0)-s_starttime
;
255 humanDuration(passed
)<<
258 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
259 (int)d_queries
.get1()<<", "<<
260 (int)d_queries
.get5()<<", "<<
261 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
264 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
265 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
266 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
267 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
268 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
271 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
272 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
273 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
274 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
275 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
278 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
279 (int)d_qcachemisses
.get1()<<", "<<
280 (int)d_qcachemisses
.get5()<<", "<<
281 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
284 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
285 if(req
->getvars
["ring"].empty()) {
286 auto entries
= S
.listRings();
287 for(const auto &i
: entries
) {
288 printtable(ret
, i
, S
.getRingTitle(i
));
292 if(arg().mustDo("webserver-print-arguments"))
295 else if(S
.ringExists(req
->getvars
["ring"]))
296 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
298 ret
<<"</div></div>"<<endl
;
299 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
300 ret
<<"</body></html>"<<endl
;
302 resp
->body
= ret
.str();
306 /** Helper to build a record content as needed. */
307 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
308 // noDot: for backend storage, pass true. for API users, pass false.
309 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
310 return drc
->getZoneRepresentation(noDot
);
313 /** "Normalize" record content for API consumers. */
314 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
315 return makeRecordContent(qtype
, content
, false);
318 /** "Normalize" record content for backend storage. */
319 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
320 return makeRecordContent(qtype
, content
, true);
323 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
*dk
) {
324 string zoneId
= apiZoneNameToId(di
.zone
);
325 vector
<string
> masters
;
326 for(const auto& m
: di
.masters
)
327 masters
.push_back(m
.toStringWithPortExcept(53));
329 return Json::object
{
330 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
332 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
333 { "name", di
.zone
.toString() },
334 { "kind", di
.getKindString() },
335 { "dnssec", dk
->isSecuredZone(di
.zone
) },
336 { "account", di
.account
},
337 { "masters", masters
},
338 { "serial", (double)di
.serial
},
339 { "notified_serial", (double)di
.notified_serial
},
340 { "last_check", (double)di
.last_check
}
344 static bool shouldDoRRSets(HttpRequest
* req
) {
345 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
347 if (req
->getvars
["rrsets"] == "false")
349 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
352 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
355 if(!B
.getDomainInfo(zonename
, di
)) {
356 throw HttpNotFoundException();
360 Json::object doc
= getZoneInfo(di
, &dk
);
361 // extra stuff getZoneInfo doesn't do for us (more expensive)
363 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
364 doc
["soa_edit_api"] = soa_edit_api
;
366 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
367 doc
["soa_edit"] = soa_edit
;
369 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
370 doc
["nsec3param"] = nsec3param
;
372 bool nsec3narrowbool
= false;
373 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
374 if (nsec3narrow
== "1")
375 nsec3narrowbool
= true;
376 doc
["nsec3narrow"] = nsec3narrowbool
;
379 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
380 doc
["api_rectify"] = (api_rectify
== "1");
383 vector
<string
> tsig_master
, tsig_slave
;
384 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
385 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
387 Json::array tsig_master_keys
;
388 for (const auto& keyname
: tsig_master
) {
389 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
391 doc
["master_tsig_key_ids"] = tsig_master_keys
;
393 Json::array tsig_slave_keys
;
394 for (const auto& keyname
: tsig_slave
) {
395 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
397 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
400 vector
<DNSResourceRecord
> records
;
401 vector
<Comment
> comments
;
403 // load all records + sort
405 DNSResourceRecord rr
;
406 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
407 while(di
.backend
->get(rr
)) {
408 if (!rr
.qtype
.getCode())
409 continue; // skip empty non-terminals
410 records
.push_back(rr
);
412 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
413 /* if you ever want to update this comparison function,
414 please be aware that you will also need to update the conditions in the code merging
415 the records and comments below */
416 if (a
.qname
== b
.qname
) {
417 return b
.qtype
< a
.qtype
;
419 return b
.qname
< a
.qname
;
423 // load all comments + sort
426 di
.backend
->listComments(di
.id
);
427 while(di
.backend
->getComment(comment
)) {
428 comments
.push_back(comment
);
430 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
431 /* if you ever want to update this comparison function,
432 please be aware that you will also need to update the conditions in the code merging
433 the records and comments below */
434 if (a
.qname
== b
.qname
) {
435 return b
.qtype
< a
.qtype
;
437 return b
.qname
< a
.qname
;
443 Json::array rrset_records
;
444 Json::array rrset_comments
;
445 DNSName current_qname
;
448 auto rit
= records
.begin();
449 auto cit
= comments
.begin();
451 while (rit
!= records
.end() || cit
!= comments
.end()) {
452 // if you think this should be rit < cit instead of cit < rit, note the b < a instead of a < b in the sort comparison functions above
453 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
454 current_qname
= rit
->qname
;
455 current_qtype
= rit
->qtype
;
458 current_qname
= cit
->qname
;
459 current_qtype
= cit
->qtype
;
463 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
464 ttl
= min(ttl
, rit
->ttl
);
465 rrset_records
.push_back(Json::object
{
466 { "disabled", rit
->disabled
},
467 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
471 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
472 rrset_comments
.push_back(Json::object
{
473 { "modified_at", (double)cit
->modified_at
},
474 { "account", cit
->account
},
475 { "content", cit
->content
}
480 rrset
["name"] = current_qname
.toString();
481 rrset
["type"] = current_qtype
.getName();
482 rrset
["records"] = rrset_records
;
483 rrset
["comments"] = rrset_comments
;
484 rrset
["ttl"] = (double)ttl
;
485 rrsets
.push_back(rrset
);
487 rrset_records
.clear();
488 rrset_comments
.clear();
491 doc
["rrsets"] = rrsets
;
497 void productServerStatisticsFetch(map
<string
,string
>& out
)
499 vector
<string
> items
= S
.getEntries();
500 for(const string
& item
: items
) {
501 out
[item
] = std::to_string(S
.read(item
));
505 out
["uptime"] = std::to_string(time(0) - s_starttime
);
508 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
511 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
519 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
520 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
521 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
525 static void gatherRecords(const string
& logprefix
, const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
527 DNSResourceRecord rr
;
533 validateGatheredRRType(rr
);
534 const auto& items
= container
["records"].array_items();
535 for(const auto& record
: items
) {
536 string content
= stringFromJson(record
, "content");
537 rr
.disabled
= boolFromJson(record
, "disabled");
539 // validate that the client sent something we can actually parse, and require that data to be dotted.
541 if (rr
.qtype
.getCode() != QType::AAAA
) {
542 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
543 if (!pdns_iequals(tmp
, content
)) {
544 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
547 struct in6_addr tmpbuf
;
548 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
549 throw std::runtime_error("Invalid IPv6 address");
552 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
554 catch(std::exception
& e
)
556 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
559 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
560 boolFromJson(record
, "set-ptr", false) == true) {
562 g_log
<<Logger::Warning
<<logprefix
<<"API call uses deprecated set-ptr feature, please remove it"<<endl
;
564 DNSResourceRecord ptr
;
567 // verify that there's a zone for the PTR
569 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
570 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
572 ptr
.domain_id
= sd
.domain_id
;
573 new_ptrs
.push_back(ptr
);
576 new_records
.push_back(rr
);
580 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
585 time_t now
= time(0);
586 for (auto comment
: container
["comments"].array_items()) {
587 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
588 c
.content
= stringFromJson(comment
, "content");
589 c
.account
= stringFromJson(comment
, "account");
590 new_comments
.push_back(c
);
594 static void checkDefaultDNSSECAlgos() {
595 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
596 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
597 int k_size
= arg().asNum("default-ksk-size");
598 int z_size
= arg().asNum("default-zsk-size");
600 // Sanity check DNSSEC parameters
601 if (::arg()["default-zsk-algorithm"] != "") {
603 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
604 else if (k_algo
<= 10 && k_size
== 0)
605 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
608 if (::arg()["default-zsk-algorithm"] != "") {
610 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
611 else if (z_algo
<= 10 && z_size
== 0)
612 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
616 static void throwUnableToSecure(const DNSName
& zonename
) {
617 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
618 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
621 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
, bool rectifyTransaction
=true) {
622 vector
<string
> zonemaster
;
623 bool shouldRectify
= false;
624 for(auto value
: document
["masters"].array_items()) {
625 string master
= value
.string_value();
627 throw ApiException("Master can not be an empty string");
629 ComboAddress
m(master
);
630 } catch (const PDNSException
&e
) {
631 throw ApiException("Master (" + master
+ ") is not an IP address: " + e
.reason
);
633 zonemaster
.push_back(master
);
636 if (zonemaster
.size()) {
637 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
639 if (document
["kind"].is_string()) {
640 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
642 if (document
["soa_edit_api"].is_string()) {
643 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
645 if (document
["soa_edit"].is_string()) {
646 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
649 bool api_rectify
= boolFromJson(document
, "api_rectify");
650 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
652 catch (const JsonException
&) {}
654 if (document
["account"].is_string()) {
655 di
.backend
->setAccount(zonename
, document
["account"].string_value());
659 bool dnssecInJSON
= false;
660 bool dnssecDocVal
= false;
663 dnssecDocVal
= boolFromJson(document
, "dnssec");
666 catch (const JsonException
&) {}
668 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
673 checkDefaultDNSSECAlgos();
675 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
676 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
677 int k_size
= arg().asNum("default-ksk-size");
678 int z_size
= arg().asNum("default-zsk-size");
682 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
683 throwUnableToSecure(zonename
);
689 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
690 throwUnableToSecure(zonename
);
694 // Used later for NSEC3PARAM
695 isDNSSECZone
= dk
.isSecuredZone(zonename
);
698 throwUnableToSecure(zonename
);
700 shouldRectify
= true;
703 // "dnssec": false in json
706 if (!dk
.unSecureZone(zonename
, error
, info
)) {
707 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
709 isDNSSECZone
= dk
.isSecuredZone(zonename
);
711 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
713 shouldRectify
= true;
718 if(document
["nsec3param"].string_value().length() > 0) {
719 shouldRectify
= true;
720 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
721 string error_msg
= "";
723 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
725 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
726 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
728 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
729 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
730 "' passed our basic sanity checks, but cannot be used with the current backend.");
734 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
737 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
738 if (api_rectify
.empty()) {
739 if (::arg().mustDo("default-api-rectify")) {
743 if (api_rectify
== "1") {
746 if (!dk
.rectifyZone(zonename
, error_msg
, info
, rectifyTransaction
)) {
747 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
752 string soa_edit_api_kind
;
753 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
754 if (!soa_edit_api_kind
.empty()) {
756 if (!B
.getSOAUncached(zonename
, sd
))
759 string soa_edit_kind
;
760 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
762 DNSResourceRecord rr
;
763 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
764 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
765 throw ApiException("Hosting backend does not support editing records.");
771 if (!document
["master_tsig_key_ids"].is_null()) {
772 vector
<string
> metadata
;
775 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
776 auto keyname(apiZoneIdToName(value
.string_value()));
777 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
778 if (keyAlgo
.empty() || keyContent
.empty()) {
779 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
781 metadata
.push_back(keyname
.toString());
783 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
784 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
787 if (!document
["slave_tsig_key_ids"].is_null()) {
788 vector
<string
> metadata
;
791 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
792 auto keyname(apiZoneIdToName(value
.string_value()));
793 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
794 if (keyAlgo
.empty() || keyContent
.empty()) {
795 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
797 metadata
.push_back(keyname
.toString());
799 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
800 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
805 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
806 static vector
<string
> builtinOptions
{
809 "ALLOW-DNSUPDATE-FROM",
810 "TSIG-ALLOW-DNSUPDATE",
812 "SOA-EDIT-DNSUPDATE",
816 "GSS-ALLOW-AXFR-PRINCIPAL",
817 "GSS-ACCEPTOR-PRINCIPAL",
827 "TSIG-ALLOW-DNSUPDATE"
830 // the following options do not allow modifications via API
831 static vector
<string
> protectedOptions
{
841 if (kind
.find("X-") == 0)
846 for (const string
& s
: builtinOptions
) {
848 for (const string
& s2
: protectedOptions
) {
849 if (!readonly
&& s
== s2
)
860 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
861 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
865 if (!B
.getDomainInfo(zonename
, di
)) {
866 throw HttpNotFoundException();
869 if (req
->method
== "GET") {
870 map
<string
, vector
<string
> > md
;
871 Json::array document
;
873 if (!B
.getAllDomainMetadata(zonename
, md
))
874 throw HttpNotFoundException();
876 for (const auto& i
: md
) {
878 for (string j
: i
.second
)
879 entries
.push_back(j
);
882 { "type", "Metadata" },
884 { "metadata", entries
}
887 document
.push_back(key
);
890 resp
->setBody(document
);
891 } else if (req
->method
== "POST") {
892 auto document
= req
->json();
894 vector
<string
> entries
;
897 kind
= stringFromJson(document
, "kind");
898 } catch (const JsonException
&) {
899 throw ApiException("kind is not specified or not a string");
902 if (!isValidMetadataKind(kind
, false))
903 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
905 vector
<string
> vecMetadata
;
907 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
908 throw ApiException("Could not retrieve metadata entries for domain '" +
909 zonename
.toString() + "'");
911 auto& metadata
= document
["metadata"];
912 if (!metadata
.is_array())
913 throw ApiException("metadata is not specified or not an array");
915 for (const auto& i
: metadata
.array_items()) {
917 throw ApiException("metadata must be strings");
918 else if (std::find(vecMetadata
.cbegin(),
920 i
.string_value()) == vecMetadata
.cend()) {
921 vecMetadata
.push_back(i
.string_value());
925 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
926 throw ApiException("Could not update metadata entries for domain '" +
927 zonename
.toString() + "'");
929 Json::array respMetadata
;
930 for (const string
& s
: vecMetadata
)
931 respMetadata
.push_back(s
);
934 { "type", "Metadata" },
935 { "kind", document
["kind"] },
936 { "metadata", respMetadata
}
942 throw HttpMethodNotAllowedException();
945 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
946 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
950 if (!B
.getDomainInfo(zonename
, di
)) {
951 throw HttpNotFoundException();
954 string kind
= req
->parameters
["kind"];
956 if (req
->method
== "GET") {
957 vector
<string
> metadata
;
958 Json::object document
;
961 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
962 throw HttpNotFoundException();
963 else if (!isValidMetadataKind(kind
, true))
964 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
966 document
["type"] = "Metadata";
967 document
["kind"] = kind
;
969 for (const string
& i
: metadata
)
970 entries
.push_back(i
);
972 document
["metadata"] = entries
;
973 resp
->setBody(document
);
974 } else if (req
->method
== "PUT") {
975 auto document
= req
->json();
977 if (!isValidMetadataKind(kind
, false))
978 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
980 vector
<string
> vecMetadata
;
981 auto& metadata
= document
["metadata"];
982 if (!metadata
.is_array())
983 throw ApiException("metadata is not specified or not an array");
985 for (const auto& i
: metadata
.array_items()) {
987 throw ApiException("metadata must be strings");
988 vecMetadata
.push_back(i
.string_value());
991 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
992 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
995 { "type", "Metadata" },
997 { "metadata", metadata
}
1001 } else if (req
->method
== "DELETE") {
1002 if (!isValidMetadataKind(kind
, false))
1003 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1005 vector
<string
> md
; // an empty vector will do it
1006 if (!B
.setDomainMetadata(zonename
, kind
, md
))
1007 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1009 throw HttpMethodNotAllowedException();
1012 // Throws 404 if the key with inquireKeyId does not exist
1013 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1014 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1016 for(const auto& value
: keyset
) {
1017 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1023 throw HttpNotFoundException();
1027 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1028 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1030 bool inquireSingleKey
= inquireKeyId
>= 0;
1033 for(const auto& value
: keyset
) {
1034 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1039 switch (value
.second
.keyType
) {
1040 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1041 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1042 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1046 { "type", "Cryptokey" },
1047 { "id", (int)value
.second
.id
},
1048 { "active", value
.second
.active
},
1049 { "keytype", keyType
},
1050 { "flags", (uint16_t)value
.first
.d_flags
},
1051 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1052 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1053 { "bits", value
.first
.getKey()->getBits() }
1056 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1058 for(const uint8_t keyid
: { DNSSECKeeper::SHA1
, DNSSECKeeper::SHA256
, DNSSECKeeper::GOST
, DNSSECKeeper::SHA384
})
1060 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1065 if (inquireSingleKey
) {
1066 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1073 if (inquireSingleKey
) {
1074 // we came here because we couldn't find the requested key.
1075 throw HttpNotFoundException();
1082 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1083 * It deletes a key from :zone_name specified by :cryptokey_id.
1085 * Case 1: the backend returns true on removal. This means the key is gone.
1086 * The server returns 204 No Content, no body.
1087 * Case 2: the backend returns false on removal. An error occurred.
1088 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1089 * Case 3: the key or zone does not exist.
1090 * The server returns 404 Not Found
1092 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1093 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1097 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1102 * This method adds a key to a zone by generate it or content parameter.
1105 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1106 * "keytype" : "ksk|zsk" <string>
1107 * "active" : "true|false" <value>
1108 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1109 * "bits" : number of bits <int>
1113 * Case 1: keytype isn't ksk|zsk
1114 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1115 * Case 2: 'bits' must be a positive integer value.
1116 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1117 * Case 3: The "algorithm" isn't supported
1118 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1119 * Case 4: Algorithm <= 10 and no bits were passed
1120 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1121 * Case 5: The wrong keysize was passed
1122 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1123 * Case 6: If the server cant guess the keysize
1124 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1125 * Case 7: The key-creation failed
1126 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1127 * Case 8: The key in content has the wrong format
1128 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1129 * Case 9: The wrong combination of fields is submitted
1130 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1131 * Case 10: No content and everything was fine
1132 * The server returns 201 Created and all public data about the new cryptokey
1133 * Case 11: With specified content
1134 * The server returns 201 Created and all public data about the added cryptokey
1137 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1138 auto document
= req
->json();
1139 string privatekey_fieldname
= "privatekey";
1140 auto privatekey
= document
["privatekey"];
1141 if (privatekey
.is_null()) {
1142 // Fallback to the old "content" behaviour
1143 privatekey
= document
["content"];
1144 privatekey_fieldname
= "content";
1146 bool active
= boolFromJson(document
, "active", false);
1149 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1151 } else if (stringFromJson(document
, "keytype") == "zsk") {
1154 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1157 int64_t insertedId
= -1;
1159 if (privatekey
.is_null()) {
1160 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1161 auto docbits
= document
["bits"];
1162 if (!docbits
.is_null()) {
1163 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1164 throw ApiException("'bits' must be a positive integer value");
1166 bits
= docbits
.int_value();
1169 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1170 auto providedAlgo
= document
["algorithm"];
1171 if (providedAlgo
.is_string()) {
1172 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1173 if (algorithm
== -1)
1174 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1175 } else if (providedAlgo
.is_number()) {
1176 algorithm
= providedAlgo
.int_value();
1177 } else if (!providedAlgo
.is_null()) {
1178 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1182 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
)) {
1183 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1185 } catch (std::runtime_error
& error
) {
1186 throw ApiException(error
.what());
1189 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1190 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1191 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1192 DNSKEYRecordContent dkrc
;
1193 DNSSECPrivateKey dpk
;
1195 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1196 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1197 // TODO remove in 4.2.0
1198 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1199 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1208 catch (std::runtime_error
& error
) {
1209 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1211 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
)) {
1212 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1214 } catch (std::runtime_error
& error
) {
1215 throw ApiException(error
.what());
1218 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1220 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1222 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1227 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1228 * It de/activates a key from :zone_name specified by :cryptokey_id.
1230 * Case 1: invalid JSON data
1231 * The server returns 400 Bad Request
1232 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1233 * The server returns 204 No Content
1234 * Case 3: the backend returns false on de/activation. An error occurred.
1235 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1237 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1238 //throws an exception if the Body is empty
1239 auto document
= req
->json();
1240 //throws an exception if the key does not exist or is not a bool
1241 bool active
= boolFromJson(document
, "active");
1243 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1244 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1248 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1249 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1259 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1260 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1261 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1263 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1264 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1267 DNSSECKeeper
dk(&B
);
1269 if (!B
.getDomainInfo(zonename
, di
)) {
1270 throw HttpNotFoundException();
1273 int inquireKeyId
= -1;
1274 if (req
->parameters
.count("key_id")) {
1275 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1276 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1279 if (req
->method
== "GET") {
1280 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1281 } else if (req
->method
== "DELETE") {
1282 if (inquireKeyId
== -1)
1283 throw HttpBadRequestException();
1284 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1285 } else if (req
->method
== "POST") {
1286 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1287 } else if (req
->method
== "PUT") {
1288 if (inquireKeyId
== -1)
1289 throw HttpBadRequestException();
1290 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1292 throw HttpMethodNotAllowedException(); //Returns method not allowed
1296 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1297 DNSResourceRecord rr
;
1298 vector
<string
> zonedata
;
1299 stringtok(zonedata
, zonestring
, "\r\n");
1301 ZoneParserTNG
zpt(zonedata
, zonename
);
1305 string comment
= "Imported via the API";
1308 while(zpt
.get(rr
, &comment
)) {
1309 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1311 if(rr
.qtype
.getCode() == QType::SOA
)
1313 validateGatheredRRType(rr
);
1315 new_records
.push_back(rr
);
1318 catch(std::exception
& ae
) {
1319 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1323 /** Throws ApiException if records which violate RRset contraints are present.
1324 * NOTE: sorts records in-place.
1326 * Constraints being checked:
1327 * *) no exact duplicates
1328 * *) no duplicates for QTypes that can only be present once per RRset
1329 * *) hostnames are hostnames
1331 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1332 sort(records
.begin(), records
.end(),
1333 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1334 /* we need _strict_ weak ordering */
1335 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1339 DNSResourceRecord previous
;
1340 for(const auto& rec
: records
) {
1341 if (previous
.qname
== rec
.qname
) {
1342 if (previous
.qtype
== rec
.qtype
) {
1343 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1344 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1346 if (previous
.content
== rec
.content
) {
1347 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1349 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1350 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1354 // Check if the DNSNames that should be hostnames, are hostnames
1356 checkHostnameCorrectness(rec
);
1357 } catch (const std::exception
& e
) {
1358 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1365 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1367 string contentFromDB
;
1368 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1369 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1370 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1374 if (!getTSIGHashEnum(algo
, the
)) {
1375 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1379 if (B64Decode(content
, b64out
) == -1) {
1380 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1384 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1385 Json::object tsigkey
= {
1386 { "name", keyname
.toStringNoDot() },
1387 { "id", apiZoneNameToId(keyname
) },
1388 { "algorithm", algo
.toStringNoDot() },
1390 { "type", "TSIGKey" }
1395 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1396 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1399 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1401 if (req
->method
== "GET") {
1402 vector
<struct TSIGKey
> keys
;
1404 if (!B
.getTSIGKeys(keys
)) {
1405 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1410 for(const auto &key
: keys
) {
1411 doc
.push_back(makeJSONTSIGKey(key
, false));
1414 } else if (req
->method
== "POST") {
1415 auto document
= req
->json();
1416 DNSName
keyname(stringFromJson(document
, "name"));
1417 DNSName
algo(stringFromJson(document
, "algorithm"));
1418 string content
= document
["key"].string_value();
1420 if (content
.empty()) {
1422 content
= makeTSIGKey(algo
);
1423 } catch (const PDNSException
& e
) {
1424 throw HttpBadRequestException(e
.reason
);
1428 // Will throw an ApiException or HttpConflictException on error
1429 checkTSIGKey(B
, keyname
, algo
, content
);
1431 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1432 throw HttpInternalServerErrorException("Unable to add TSIG key");
1436 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1438 throw HttpMethodNotAllowedException();
1442 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1444 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1448 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1449 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1454 tsk
.algorithm
= algo
;
1457 if (req
->method
== "GET") {
1458 resp
->setBody(makeJSONTSIGKey(tsk
));
1459 } else if (req
->method
== "PUT") {
1460 json11::Json document
;
1461 if (!req
->body
.empty()) {
1462 document
= req
->json();
1464 if (document
["name"].is_string()) {
1465 tsk
.name
= DNSName(document
["name"].string_value());
1467 if (document
["algorithm"].is_string()) {
1468 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1471 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1472 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1475 if (document
["key"].is_string()) {
1476 string new_content
= document
["key"].string_value();
1478 if (B64Decode(new_content
, decoded
) == -1) {
1479 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1481 tsk
.key
= new_content
;
1483 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1484 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1486 if (tsk
.name
!= keyname
) {
1487 // Remove the old key
1488 if (!B
.deleteTSIGKey(keyname
)) {
1489 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1492 resp
->setBody(makeJSONTSIGKey(tsk
));
1493 } else if (req
->method
== "DELETE") {
1494 if (!B
.deleteTSIGKey(keyname
)) {
1495 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1501 throw HttpMethodNotAllowedException();
1505 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1507 DNSSECKeeper
dk(&B
);
1508 if (req
->method
== "POST") {
1510 auto document
= req
->json();
1511 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1512 apiCheckNameAllowedCharacters(zonename
.toString());
1513 zonename
.makeUsLowerCase();
1515 bool exists
= B
.getDomainInfo(zonename
, di
);
1517 throw HttpConflictException();
1519 // validate 'kind' is set
1520 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1522 string zonestring
= document
["zone"].string_value();
1523 auto rrsets
= document
["rrsets"];
1524 if (rrsets
.is_array() && zonestring
!= "")
1525 throw ApiException("You cannot give rrsets AND zone data as text");
1527 auto nameservers
= document
["nameservers"];
1528 if (!nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1529 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
1531 string soa_edit_api_kind
;
1532 if (document
["soa_edit_api"].is_string()) {
1533 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1536 soa_edit_api_kind
= "DEFAULT";
1538 string soa_edit_kind
= document
["soa_edit"].string_value();
1540 // if records/comments are given, load and check them
1541 bool have_soa
= false;
1542 bool have_zone_ns
= false;
1543 vector
<DNSResourceRecord
> new_records
;
1544 vector
<Comment
> new_comments
;
1545 vector
<DNSResourceRecord
> new_ptrs
;
1547 if (rrsets
.is_array()) {
1548 for (const auto& rrset
: rrsets
.array_items()) {
1549 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1550 apiCheckQNameAllowedCharacters(qname
.toString());
1552 qtype
= stringFromJson(rrset
, "type");
1553 if (qtype
.getCode() == 0) {
1554 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1556 if (rrset
["records"].is_array()) {
1557 int ttl
= intFromJson(rrset
, "ttl");
1558 gatherRecords(req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1560 if (rrset
["comments"].is_array()) {
1561 gatherComments(rrset
, qname
, qtype
, new_comments
);
1564 } else if (zonestring
!= "") {
1565 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1568 for(auto& rr
: new_records
) {
1569 rr
.qname
.makeUsLowerCase();
1570 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1571 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1572 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1574 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1576 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1578 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1579 have_zone_ns
= true;
1583 // synthesize RRs as needed
1584 DNSResourceRecord autorr
;
1585 autorr
.qname
= zonename
;
1587 autorr
.ttl
= ::arg().asNum("default-ttl");
1589 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1590 // synthesize a SOA record so the zone "really" exists
1591 string soa
= (boost::format("%s %s %ul")
1592 % ::arg()["default-soa-name"]
1593 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1594 % document
["serial"].int_value()
1597 fillSOAData(soa
, sd
); // fills out default values for us
1598 autorr
.qtype
= QType::SOA
;
1599 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1600 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1601 new_records
.push_back(autorr
);
1604 // create NS records if nameservers are given
1605 for (auto value
: nameservers
.array_items()) {
1606 string nameserver
= value
.string_value();
1607 if (nameserver
.empty())
1608 throw ApiException("Nameservers must be non-empty strings");
1609 if (!isCanonical(nameserver
))
1610 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1612 // ensure the name parses
1613 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1615 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1617 autorr
.qtype
= QType::NS
;
1618 new_records
.push_back(autorr
);
1620 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1624 checkNewRecords(new_records
);
1626 if (boolFromJson(document
, "dnssec", false)) {
1627 checkDefaultDNSSECAlgos();
1629 if(document
["nsec3param"].string_value().length() > 0) {
1630 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1631 string error_msg
= "";
1632 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1633 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1638 // no going back after this
1639 if(!B
.createDomain(zonename
))
1640 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1642 if(!B
.getDomainInfo(zonename
, di
))
1643 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1645 di
.backend
->startTransaction(zonename
, di
.id
);
1647 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1648 if (!soa_edit_api_kind
.empty()) {
1649 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1652 for(auto rr
: new_records
) {
1653 rr
.domain_id
= di
.id
;
1654 di
.backend
->feedRecord(rr
, DNSName());
1656 for(Comment
& c
: new_comments
) {
1657 c
.domain_id
= di
.id
;
1658 di
.backend
->feedComment(c
);
1661 updateDomainSettingsFromDocument(B
, di
, zonename
, document
, false);
1663 di
.backend
->commitTransaction();
1665 storeChangedPTRs(B
, new_ptrs
);
1667 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1672 if(req
->method
!= "GET")
1673 throw HttpMethodNotAllowedException();
1675 vector
<DomainInfo
> domains
;
1677 if (req
->getvars
.count("zone")) {
1678 string zone
= req
->getvars
["zone"];
1679 apiCheckNameAllowedCharacters(zone
);
1680 DNSName zonename
= apiNameToDNSName(zone
);
1681 zonename
.makeUsLowerCase();
1683 if (B
.getDomainInfo(zonename
, di
)) {
1684 domains
.push_back(di
);
1688 B
.getAllDomains(&domains
, true); // incl. disabled
1689 } catch(const PDNSException
&e
) {
1690 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + e
.reason
);
1695 for(const DomainInfo
& di
: domains
) {
1696 doc
.push_back(getZoneInfo(di
, &dk
));
1701 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1702 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1707 if (!B
.getDomainInfo(zonename
, di
)) {
1708 throw HttpNotFoundException();
1710 } catch(const PDNSException
&e
) {
1711 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1714 if(req
->method
== "PUT") {
1715 // update domain settings
1717 di
.backend
->startTransaction(zonename
, -1);
1718 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json(), false);
1719 di
.backend
->commitTransaction();
1722 resp
->status
= 204; // No Content, but indicate success
1725 else if(req
->method
== "DELETE") {
1727 if(!di
.backend
->deleteDomain(zonename
))
1728 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1731 DNSSECKeeper
dk(&B
);
1732 dk
.clearCaches(zonename
);
1733 purgeAuthCaches(zonename
.toString() + "$");
1735 // empty body on success
1737 resp
->status
= 204; // No Content: declare that the zone is gone now
1739 } else if (req
->method
== "PATCH") {
1740 patchZone(req
, resp
);
1742 } else if (req
->method
== "GET") {
1743 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1746 throw HttpMethodNotAllowedException();
1749 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1750 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1752 if(req
->method
!= "GET")
1753 throw HttpMethodNotAllowedException();
1759 if (!B
.getDomainInfo(zonename
, di
)) {
1760 throw HttpNotFoundException();
1763 DNSResourceRecord rr
;
1765 di
.backend
->list(zonename
, di
.id
);
1766 while(di
.backend
->get(rr
)) {
1767 if (!rr
.qtype
.getCode())
1768 continue; // skip empty non-terminals
1771 rr
.qname
.toString() << "\t" <<
1774 rr
.qtype
.getName() << "\t" <<
1775 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1779 if (req
->accept_json
) {
1780 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1782 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1783 resp
->body
= ss
.str();
1787 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1788 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1790 if(req
->method
!= "PUT")
1791 throw HttpMethodNotAllowedException();
1795 if (!B
.getDomainInfo(zonename
, di
)) {
1796 throw HttpNotFoundException();
1799 if(di
.masters
.empty())
1800 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1802 random_shuffle(di
.masters
.begin(), di
.masters
.end());
1803 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1804 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1807 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1808 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1810 if(req
->method
!= "PUT")
1811 throw HttpMethodNotAllowedException();
1815 if (!B
.getDomainInfo(zonename
, di
)) {
1816 throw HttpNotFoundException();
1819 if(!Communicator
.notifyDomain(zonename
))
1820 throw ApiException("Failed to add to the queue - see server log");
1822 resp
->setSuccessResult("Notification queued");
1825 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1826 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1828 if(req
->method
!= "PUT")
1829 throw HttpMethodNotAllowedException();
1833 if (!B
.getDomainInfo(zonename
, di
)) {
1834 throw HttpNotFoundException();
1837 DNSSECKeeper
dk(&B
);
1839 if (!dk
.isSecuredZone(zonename
))
1840 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1842 if (di
.kind
== DomainInfo::Slave
)
1843 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1845 string error_msg
= "";
1847 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1848 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1850 resp
->setSuccessResult("Rectified");
1853 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1854 if (rr
.qtype
.getCode() == QType::A
) {
1856 if (!IpToU32(rr
.content
, &ip
)) {
1857 throw ApiException("PTR: Invalid IP address given");
1859 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1860 % ((ip
>> 24) & 0xff)
1861 % ((ip
>> 16) & 0xff)
1862 % ((ip
>> 8) & 0xff)
1865 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1866 ComboAddress
ca(rr
.content
);
1869 for (int octet
= 0; octet
< 16; ++octet
) {
1870 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1871 // this should be impossible: no byte should give more than two digits in hex format
1872 throw PDNSException("Formatting IPv6 address failed");
1874 ss
<< buf
[0] << '.' << buf
[1] << '.';
1876 string tmp
= ss
.str();
1877 tmp
.resize(tmp
.size()-1); // remove last dot
1878 // reverse and append arpa domain
1879 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1881 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1886 ptr
->disabled
= rr
.disabled
;
1887 ptr
->content
= rr
.qname
.toStringRootDot();
1890 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1891 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1893 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1894 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1896 string soa_edit_api_kind
;
1897 string soa_edit_kind
;
1898 bool soa_changed
= false;
1899 DNSResourceRecord soarr
;
1900 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1901 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1902 if (!soa_edit_api_kind
.empty()) {
1903 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1906 sd
.db
->startTransaction(sd
.qname
);
1907 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1908 sd
.db
->abortTransaction();
1909 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1913 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1916 sd
.db
->commitTransaction();
1917 purgeAuthCachesExact(rr
.qname
);
1921 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
) {
1924 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1925 if (!B
.getDomainInfo(zonename
, di
)) {
1926 throw HttpNotFoundException();
1929 vector
<DNSResourceRecord
> new_records
;
1930 vector
<Comment
> new_comments
;
1931 vector
<DNSResourceRecord
> new_ptrs
;
1933 Json document
= req
->json();
1935 auto rrsets
= document
["rrsets"];
1936 if (!rrsets
.is_array())
1937 throw ApiException("No rrsets given in update request");
1939 di
.backend
->startTransaction(zonename
);
1942 string soa_edit_api_kind
;
1943 string soa_edit_kind
;
1944 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1945 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1946 bool soa_edit_done
= false;
1948 set
<pair
<DNSName
, QType
>> seen
;
1950 for (const auto& rrset
: rrsets
.array_items()) {
1951 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1952 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1953 apiCheckQNameAllowedCharacters(qname
.toString());
1955 qtype
= stringFromJson(rrset
, "type");
1956 if (qtype
.getCode() == 0) {
1957 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1960 if(seen
.count({qname
, qtype
}))
1962 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1964 seen
.insert({qname
, qtype
});
1966 if (changetype
== "DELETE") {
1967 // delete all matching qname/qtype RRs (and, implicitly comments).
1968 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1969 throw ApiException("Hosting backend does not support editing records.");
1972 else if (changetype
== "REPLACE") {
1973 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1974 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
1975 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
1977 bool replace_records
= rrset
["records"].is_array();
1978 bool replace_comments
= rrset
["comments"].is_array();
1980 if (!replace_records
&& !replace_comments
) {
1981 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
1984 new_records
.clear();
1985 new_comments
.clear();
1987 if (replace_records
) {
1988 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1989 int ttl
= intFromJson(rrset
, "ttl");
1990 // new_ptrs is merged.
1991 gatherRecords(req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1993 for(DNSResourceRecord
& rr
: new_records
) {
1994 rr
.domain_id
= di
.id
;
1995 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1996 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1999 checkNewRecords(new_records
);
2002 if (replace_comments
) {
2003 gatherComments(rrset
, qname
, qtype
, new_comments
);
2005 for(Comment
& c
: new_comments
) {
2006 c
.domain_id
= di
.id
;
2010 if (replace_records
) {
2011 bool ent_present
= false;
2012 di
.backend
->lookup(QType(QType::ANY
), qname
);
2013 DNSResourceRecord rr
;
2014 while (di
.backend
->get(rr
)) {
2015 if (rr
.qtype
.getCode() == QType::ENT
) {
2017 /* that's fine, we will override it */
2020 if (qtype
.getCode() != rr
.qtype
.getCode()
2021 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2022 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2023 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2027 if (!new_records
.empty() && ent_present
) {
2029 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2030 throw ApiException("Hosting backend does not support editing records.");
2033 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2034 throw ApiException("Hosting backend does not support editing records.");
2037 if (replace_comments
) {
2038 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2039 throw ApiException("Hosting backend does not support editing comments.");
2044 throw ApiException("Changetype not understood");
2047 // edit SOA (if needed)
2048 if (!soa_edit_api_kind
.empty() && !soa_edit_done
) {
2050 if (!B
.getSOAUncached(zonename
, sd
))
2051 throw ApiException("No SOA found for domain '"+zonename
.toString()+"'");
2053 DNSResourceRecord rr
;
2054 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2055 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2056 throw ApiException("Hosting backend does not support editing records.");
2060 // return old and new serials in headers
2061 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2062 fillSOAData(rr
.content
, sd
);
2063 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2067 di
.backend
->abortTransaction();
2071 DNSSECKeeper
dk(&B
);
2073 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
2074 if (dk
.isSecuredZone(zonename
) && !dk
.isPresigned(zonename
) && api_rectify
== "1") {
2075 string error_msg
= "";
2077 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false))
2078 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2081 di
.backend
->commitTransaction();
2083 purgeAuthCachesExact(zonename
);
2086 storeChangedPTRs(B
, new_ptrs
);
2089 resp
->status
= 204; // No Content, but indicate success
2093 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2094 if(req
->method
!= "GET")
2095 throw HttpMethodNotAllowedException();
2097 string q
= req
->getvars
["q"];
2098 string sMax
= req
->getvars
["max"];
2099 string sObjectType
= req
->getvars
["object_type"];
2104 // the following types of data can be searched for using the api
2105 enum class ObjectType
2114 throw ApiException("Query q can't be blank");
2116 maxEnts
= std::stoi(sMax
);
2118 throw ApiException("Maximum entries must be larger than 0");
2120 if (sObjectType
.empty())
2121 objectType
= ObjectType::ALL
;
2122 else if (sObjectType
== "all")
2123 objectType
= ObjectType::ALL
;
2124 else if (sObjectType
== "zone")
2125 objectType
= ObjectType::ZONE
;
2126 else if (sObjectType
== "record")
2127 objectType
= ObjectType::RECORD
;
2128 else if (sObjectType
== "comment")
2129 objectType
= ObjectType::COMMENT
;
2131 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2133 SimpleMatch
sm(q
,true);
2135 vector
<DomainInfo
> domains
;
2136 vector
<DNSResourceRecord
> result_rr
;
2137 vector
<Comment
> result_c
;
2138 map
<int,DomainInfo
> zoneIdZone
;
2139 map
<int,DomainInfo
>::iterator val
;
2142 B
.getAllDomains(&domains
, true);
2144 for(const DomainInfo di
: domains
)
2146 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2147 doc
.push_back(Json::object
{
2148 { "object_type", "zone" },
2149 { "zone_id", apiZoneNameToId(di
.zone
) },
2150 { "name", di
.zone
.toString() }
2154 zoneIdZone
[di
.id
] = di
; // populate cache
2157 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2159 for(const DNSResourceRecord
& rr
: result_rr
)
2161 if (!rr
.qtype
.getCode())
2162 continue; // skip empty non-terminals
2164 auto object
= Json::object
{
2165 { "object_type", "record" },
2166 { "name", rr
.qname
.toString() },
2167 { "type", rr
.qtype
.getName() },
2168 { "ttl", (double)rr
.ttl
},
2169 { "disabled", rr
.disabled
},
2170 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2172 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2173 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2174 object
["zone"] = val
->second
.zone
.toString();
2176 doc
.push_back(object
);
2180 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2182 for(const Comment
&c
: result_c
)
2184 auto object
= Json::object
{
2185 { "object_type", "comment" },
2186 { "name", c
.qname
.toString() },
2187 { "content", c
.content
}
2189 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2190 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2191 object
["zone"] = val
->second
.zone
.toString();
2193 doc
.push_back(object
);
2200 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2201 if(req
->method
!= "PUT")
2202 throw HttpMethodNotAllowedException();
2204 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2206 uint64_t count
= purgeAuthCachesExact(canon
);
2207 resp
->setBody(Json::object
{
2208 { "count", (int) count
},
2209 { "result", "Flushed cache." }
2213 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2215 resp
->headers
["Cache-Control"] = "max-age=86400";
2216 resp
->headers
["Content-Type"] = "text/css";
2219 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2220 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2221 ret
<<"a { color: #0959c2; }"<<endl
;
2222 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2223 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2224 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2225 ret
<<".row:after { clear: both; }"<<endl
;
2226 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2227 ret
<<".all { width: 100%; }"<<endl
;
2228 ret
<<".headl { width: 60%; }"<<endl
;
2229 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2230 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=);";
2231 ret
<<" width: 154px; height: 20px; }"<<endl
;
2232 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2233 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2234 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2235 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2236 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2237 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2238 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2239 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2240 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2241 ret
<<".resetring {float: right; }"<<endl
;
2242 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
;
2243 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2244 ret
<<".resizering {float: right;}"<<endl
;
2245 resp
->body
= ret
.str();
2249 void AuthWebServer::webThread()
2252 setThreadName("pdns/webserver");
2253 if(::arg().mustDo("api")) {
2254 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2255 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2256 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2257 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2258 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2259 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2260 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2261 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2262 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2263 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2264 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2265 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2266 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2267 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2268 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2269 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2270 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2271 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2272 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2274 if (::arg().mustDo("webserver")) {
2275 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2276 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2281 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;