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(UeberBackend
& B
, HttpRequest
* req
, HttpResponse
* resp
);
56 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
);
57 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
);
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
};
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"]);
81 d_ws
->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
87 void AuthWebServer::go()
90 pthread_create(&d_tid
, 0, webThreadHelper
, this);
91 pthread_create(&d_tid
, 0, statThreadHelper
, this);
94 void AuthWebServer::statThread()
97 setThreadName("pdns/statHelper");
99 d_queries
.submit(S
.read("udp-queries"));
100 d_cachehits
.submit(S
.read("packetcache-hit"));
101 d_cachemisses
.submit(S
.read("packetcache-miss"));
102 d_qcachehits
.submit(S
.read("query-cache-hit"));
103 d_qcachemisses
.submit(S
.read("query-cache-miss"));
108 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
113 void *AuthWebServer::statThreadHelper(void *p
)
115 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
117 return 0; // never reached
120 void *AuthWebServer::webThreadHelper(void *p
)
122 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
124 return 0; // never reached
127 static string
htmlescape(const string
&s
) {
129 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
150 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
154 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
156 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
161 ret
<<"<div class=\"panel\">";
162 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
163 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
164 ret
<<"<div class=ringmeta>";
165 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
166 ret
<<"<span class=resizering>Resize: ";
167 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
168 for(int i
=0;sizes
[i
];++i
) {
169 if(S
.getRingSize(ringname
)!=sizes
[i
])
170 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
172 ret
<<"("<<sizes
[i
]<<") ";
174 ret
<<"</span></div>";
176 ret
<<"<table class=\"data\">";
178 int total
=max(1,tot
);
179 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
180 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
183 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
185 ret
<<"<tr><td><b>Rest:</b></td><td><b>"<<tot
-printed
<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot
-printed
)*100.0/total
)<<"</b></td>"<<endl
;
187 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
188 ret
<<"</table></div>"<<endl
;
191 void AuthWebServer::printvars(ostringstream
&ret
)
193 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
195 vector
<string
>entries
=S
.getEntries();
196 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
197 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
200 ret
<<"</table></div>"<<endl
;
203 void AuthWebServer::printargs(ostringstream
&ret
)
205 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
207 vector
<string
>entries
=arg().list();
208 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
209 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
213 string
AuthWebServer::makePercentage(const double& val
)
215 return (boost::format("%.01f%%") % val
).str();
218 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
220 if(!req
->getvars
["resetring"].empty()) {
221 if (S
.ringExists(req
->getvars
["resetring"]))
222 S
.resetRing(req
->getvars
["resetring"]);
224 resp
->headers
["Location"] = req
->url
.path
;
227 if(!req
->getvars
["resizering"].empty()){
228 int size
=std::stoi(req
->getvars
["size"]);
229 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
230 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
232 resp
->headers
["Location"] = req
->url
.path
;
238 ret
<<"<!DOCTYPE html>"<<endl
;
239 ret
<<"<html><head>"<<endl
;
240 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
241 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
242 ret
<<"</head><body>"<<endl
;
244 ret
<<"<div class=\"row\">"<<endl
;
245 ret
<<"<div class=\"headl columns\">";
246 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
247 if(!arg()["config-name"].empty()) {
248 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
250 ret
<<"</a></div>"<<endl
;
251 ret
<<"<div class=\"headr columns\"></div></div>";
252 ret
<<"<div class=\"row\"><div class=\"all columns\">";
254 time_t passed
=time(0)-s_starttime
;
257 humanDuration(passed
)<<
260 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
261 (int)d_queries
.get1()<<", "<<
262 (int)d_queries
.get5()<<", "<<
263 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
266 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
267 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
268 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
269 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
270 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
273 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
274 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
275 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
276 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
277 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
280 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
281 (int)d_qcachemisses
.get1()<<", "<<
282 (int)d_qcachemisses
.get5()<<", "<<
283 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
286 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
287 if(req
->getvars
["ring"].empty()) {
288 auto entries
= S
.listRings();
289 for(const auto &i
: entries
) {
290 printtable(ret
, i
, S
.getRingTitle(i
));
294 if(arg().mustDo("webserver-print-arguments"))
297 else if(S
.ringExists(req
->getvars
["ring"]))
298 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
300 ret
<<"</div></div>"<<endl
;
301 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
302 ret
<<"</body></html>"<<endl
;
304 resp
->body
= ret
.str();
308 /** Helper to build a record content as needed. */
309 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
310 // noDot: for backend storage, pass true. for API users, pass false.
311 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
312 return drc
->getZoneRepresentation(noDot
);
315 /** "Normalize" record content for API consumers. */
316 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
317 return makeRecordContent(qtype
, content
, false);
320 /** "Normalize" record content for backend storage. */
321 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
322 return makeRecordContent(qtype
, content
, true);
325 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
* dk
) {
326 string zoneId
= apiZoneNameToId(di
.zone
);
327 vector
<string
> masters
;
328 masters
.reserve(di
.masters
.size());
329 for(const auto& m
: di
.masters
) {
330 masters
.push_back(m
.toStringWithPortExcept(53));
333 auto obj
= Json::object
{
334 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
336 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
337 { "name", di
.zone
.toString() },
338 { "kind", di
.getKindString() },
339 { "account", di
.account
},
340 { "masters", std::move(masters
) },
341 { "serial", (double)di
.serial
},
342 { "notified_serial", (double)di
.notified_serial
},
343 { "last_check", (double)di
.last_check
}
346 obj
["dnssec"] = dk
->isSecuredZone(di
.zone
);
347 obj
["edited_serial"] = (double)calculateEditSOA(di
.serial
, *dk
, di
.zone
);
352 static bool shouldDoRRSets(HttpRequest
* req
) {
353 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
355 if (req
->getvars
["rrsets"] == "false")
357 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
360 static void fillZone(UeberBackend
& B
, const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
362 if(!B
.getDomainInfo(zonename
, di
)) {
363 throw HttpNotFoundException();
367 Json::object doc
= getZoneInfo(di
, &dk
);
368 // extra stuff getZoneInfo doesn't do for us (more expensive)
370 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
371 doc
["soa_edit_api"] = soa_edit_api
;
373 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
374 doc
["soa_edit"] = soa_edit
;
376 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
377 doc
["nsec3param"] = nsec3param
;
379 bool nsec3narrowbool
= false;
380 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
381 if (nsec3narrow
== "1")
382 nsec3narrowbool
= true;
383 doc
["nsec3narrow"] = nsec3narrowbool
;
384 doc
["dnssec"] = dk
.isSecuredZone(zonename
);
387 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
388 doc
["api_rectify"] = (api_rectify
== "1");
391 vector
<string
> tsig_master
, tsig_slave
;
392 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
393 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
395 Json::array tsig_master_keys
;
396 for (const auto& keyname
: tsig_master
) {
397 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
399 doc
["master_tsig_key_ids"] = tsig_master_keys
;
401 Json::array tsig_slave_keys
;
402 for (const auto& keyname
: tsig_slave
) {
403 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
405 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
408 vector
<DNSResourceRecord
> records
;
409 vector
<Comment
> comments
;
411 // load all records + sort
413 DNSResourceRecord rr
;
414 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
415 while(di
.backend
->get(rr
)) {
416 if (!rr
.qtype
.getCode())
417 continue; // skip empty non-terminals
418 records
.push_back(rr
);
420 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
421 /* if you ever want to update this comparison function,
422 please be aware that you will also need to update the conditions in the code merging
423 the records and comments below */
424 if (a
.qname
== b
.qname
) {
425 return b
.qtype
< a
.qtype
;
427 return b
.qname
< a
.qname
;
431 // load all comments + sort
434 di
.backend
->listComments(di
.id
);
435 while(di
.backend
->getComment(comment
)) {
436 comments
.push_back(comment
);
438 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
439 /* if you ever want to update this comparison function,
440 please be aware that you will also need to update the conditions in the code merging
441 the records and comments below */
442 if (a
.qname
== b
.qname
) {
443 return b
.qtype
< a
.qtype
;
445 return b
.qname
< a
.qname
;
451 Json::array rrset_records
;
452 Json::array rrset_comments
;
453 DNSName current_qname
;
456 auto rit
= records
.begin();
457 auto cit
= comments
.begin();
459 while (rit
!= records
.end() || cit
!= comments
.end()) {
460 // 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
461 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
462 current_qname
= rit
->qname
;
463 current_qtype
= rit
->qtype
;
466 current_qname
= cit
->qname
;
467 current_qtype
= cit
->qtype
;
471 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
472 ttl
= min(ttl
, rit
->ttl
);
473 rrset_records
.push_back(Json::object
{
474 { "disabled", rit
->disabled
},
475 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
479 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
480 rrset_comments
.push_back(Json::object
{
481 { "modified_at", (double)cit
->modified_at
},
482 { "account", cit
->account
},
483 { "content", cit
->content
}
488 rrset
["name"] = current_qname
.toString();
489 rrset
["type"] = current_qtype
.getName();
490 rrset
["records"] = rrset_records
;
491 rrset
["comments"] = rrset_comments
;
492 rrset
["ttl"] = (double)ttl
;
493 rrsets
.push_back(rrset
);
495 rrset_records
.clear();
496 rrset_comments
.clear();
499 doc
["rrsets"] = rrsets
;
505 void productServerStatisticsFetch(map
<string
,string
>& out
)
507 vector
<string
> items
= S
.getEntries();
508 for(const string
& item
: items
) {
509 out
[item
] = std::to_string(S
.read(item
));
513 out
["uptime"] = std::to_string(time(0) - s_starttime
);
516 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
519 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
527 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
528 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
529 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
533 static void gatherRecords(UeberBackend
& B
, const string
& logprefix
, const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
534 DNSResourceRecord rr
;
540 validateGatheredRRType(rr
);
541 const auto& items
= container
["records"].array_items();
542 for(const auto& record
: items
) {
543 string content
= stringFromJson(record
, "content");
545 if(!record
["disabled"].is_null()) {
546 rr
.disabled
= boolFromJson(record
, "disabled");
549 // validate that the client sent something we can actually parse, and require that data to be dotted.
551 if (rr
.qtype
.getCode() != QType::AAAA
) {
552 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
553 if (!pdns_iequals(tmp
, content
)) {
554 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
557 struct in6_addr tmpbuf
;
558 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
559 throw std::runtime_error("Invalid IPv6 address");
562 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
564 catch(std::exception
& e
)
566 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
569 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
570 boolFromJson(record
, "set-ptr", false) == true) {
572 g_log
<<Logger::Warning
<<logprefix
<<"API call uses deprecated set-ptr feature, please remove it"<<endl
;
574 DNSResourceRecord ptr
;
577 // verify that there's a zone for the PTR
579 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
580 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
582 ptr
.domain_id
= sd
.domain_id
;
583 new_ptrs
.push_back(ptr
);
586 new_records
.push_back(rr
);
590 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
595 time_t now
= time(0);
596 for (auto comment
: container
["comments"].array_items()) {
597 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
598 c
.content
= stringFromJson(comment
, "content");
599 c
.account
= stringFromJson(comment
, "account");
600 new_comments
.push_back(c
);
604 static void checkDefaultDNSSECAlgos() {
605 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
606 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
607 int k_size
= arg().asNum("default-ksk-size");
608 int z_size
= arg().asNum("default-zsk-size");
610 // Sanity check DNSSEC parameters
611 if (::arg()["default-zsk-algorithm"] != "") {
613 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
614 else if (k_algo
<= 10 && k_size
== 0)
615 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
618 if (::arg()["default-zsk-algorithm"] != "") {
620 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
621 else if (z_algo
<= 10 && z_size
== 0)
622 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
626 static void throwUnableToSecure(const DNSName
& zonename
) {
627 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
628 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
631 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
, bool rectifyTransaction
=true) {
632 vector
<string
> zonemaster
;
633 bool shouldRectify
= false;
634 for(auto value
: document
["masters"].array_items()) {
635 string master
= value
.string_value();
637 throw ApiException("Master can not be an empty string");
639 ComboAddress
m(master
);
640 } catch (const PDNSException
&e
) {
641 throw ApiException("Master (" + master
+ ") is not an IP address: " + e
.reason
);
643 zonemaster
.push_back(master
);
646 if (zonemaster
.size()) {
647 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
649 if (document
["kind"].is_string()) {
650 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
652 if (document
["soa_edit_api"].is_string()) {
653 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
655 if (document
["soa_edit"].is_string()) {
656 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
659 bool api_rectify
= boolFromJson(document
, "api_rectify");
660 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
662 catch (const JsonException
&) {}
664 if (document
["account"].is_string()) {
665 di
.backend
->setAccount(zonename
, document
["account"].string_value());
669 bool dnssecInJSON
= false;
670 bool dnssecDocVal
= false;
673 dnssecDocVal
= boolFromJson(document
, "dnssec");
676 catch (const JsonException
&) {}
678 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
683 checkDefaultDNSSECAlgos();
685 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
686 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
687 int k_size
= arg().asNum("default-ksk-size");
688 int z_size
= arg().asNum("default-zsk-size");
692 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
693 throwUnableToSecure(zonename
);
699 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
700 throwUnableToSecure(zonename
);
704 // Used later for NSEC3PARAM
705 isDNSSECZone
= dk
.isSecuredZone(zonename
);
708 throwUnableToSecure(zonename
);
710 shouldRectify
= true;
713 // "dnssec": false in json
716 if (!dk
.unSecureZone(zonename
, error
, info
)) {
717 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
719 isDNSSECZone
= dk
.isSecuredZone(zonename
);
721 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
723 shouldRectify
= true;
728 if(document
["nsec3param"].string_value().length() > 0) {
729 shouldRectify
= true;
730 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
731 string error_msg
= "";
733 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
735 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
736 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
738 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
739 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
740 "' passed our basic sanity checks, but cannot be used with the current backend.");
744 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
747 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
748 if (api_rectify
.empty()) {
749 if (::arg().mustDo("default-api-rectify")) {
753 if (api_rectify
== "1") {
756 if (!dk
.rectifyZone(zonename
, error_msg
, info
, rectifyTransaction
)) {
757 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
762 string soa_edit_api_kind
;
763 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
764 if (!soa_edit_api_kind
.empty()) {
766 if (!B
.getSOAUncached(zonename
, sd
))
769 string soa_edit_kind
;
770 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
772 DNSResourceRecord rr
;
773 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
774 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
775 throw ApiException("Hosting backend does not support editing records.");
781 if (!document
["master_tsig_key_ids"].is_null()) {
782 vector
<string
> metadata
;
783 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
784 auto keyname(apiZoneIdToName(value
.string_value()));
787 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
788 if (keyAlgo
.empty() || keyContent
.empty()) {
789 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
791 metadata
.push_back(keyname
.toString());
793 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
794 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
797 if (!document
["slave_tsig_key_ids"].is_null()) {
798 vector
<string
> metadata
;
799 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
800 auto keyname(apiZoneIdToName(value
.string_value()));
803 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
804 if (keyAlgo
.empty() || keyContent
.empty()) {
805 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
807 metadata
.push_back(keyname
.toString());
809 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
810 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
815 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
816 static vector
<string
> builtinOptions
{
819 "ALLOW-DNSUPDATE-FROM",
820 "TSIG-ALLOW-DNSUPDATE",
822 "SOA-EDIT-DNSUPDATE",
826 "GSS-ALLOW-AXFR-PRINCIPAL",
827 "GSS-ACCEPTOR-PRINCIPAL",
838 "TSIG-ALLOW-DNSUPDATE"
841 // the following options do not allow modifications via API
842 static vector
<string
> protectedOptions
{
852 if (kind
.find("X-") == 0)
857 for (const string
& s
: builtinOptions
) {
859 for (const string
& s2
: protectedOptions
) {
860 if (!readonly
&& s
== s2
)
871 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
872 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
876 if (!B
.getDomainInfo(zonename
, di
)) {
877 throw HttpNotFoundException();
880 if (req
->method
== "GET") {
881 map
<string
, vector
<string
> > md
;
882 Json::array document
;
884 if (!B
.getAllDomainMetadata(zonename
, md
))
885 throw HttpNotFoundException();
887 for (const auto& i
: md
) {
889 for (string j
: i
.second
)
890 entries
.push_back(j
);
893 { "type", "Metadata" },
895 { "metadata", entries
}
898 document
.push_back(key
);
901 resp
->setBody(document
);
902 } else if (req
->method
== "POST") {
903 auto document
= req
->json();
905 vector
<string
> entries
;
908 kind
= stringFromJson(document
, "kind");
909 } catch (const JsonException
&) {
910 throw ApiException("kind is not specified or not a string");
913 if (!isValidMetadataKind(kind
, false))
914 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
916 vector
<string
> vecMetadata
;
918 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
919 throw ApiException("Could not retrieve metadata entries for domain '" +
920 zonename
.toString() + "'");
922 auto& metadata
= document
["metadata"];
923 if (!metadata
.is_array())
924 throw ApiException("metadata is not specified or not an array");
926 for (const auto& i
: metadata
.array_items()) {
928 throw ApiException("metadata must be strings");
929 else if (std::find(vecMetadata
.cbegin(),
931 i
.string_value()) == vecMetadata
.cend()) {
932 vecMetadata
.push_back(i
.string_value());
936 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
937 throw ApiException("Could not update metadata entries for domain '" +
938 zonename
.toString() + "'");
940 Json::array respMetadata
;
941 for (const string
& s
: vecMetadata
)
942 respMetadata
.push_back(s
);
945 { "type", "Metadata" },
946 { "kind", document
["kind"] },
947 { "metadata", respMetadata
}
953 throw HttpMethodNotAllowedException();
956 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
957 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
961 if (!B
.getDomainInfo(zonename
, di
)) {
962 throw HttpNotFoundException();
965 string kind
= req
->parameters
["kind"];
967 if (req
->method
== "GET") {
968 vector
<string
> metadata
;
969 Json::object document
;
972 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
973 throw HttpNotFoundException();
974 else if (!isValidMetadataKind(kind
, true))
975 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
977 document
["type"] = "Metadata";
978 document
["kind"] = kind
;
980 for (const string
& i
: metadata
)
981 entries
.push_back(i
);
983 document
["metadata"] = entries
;
984 resp
->setBody(document
);
985 } else if (req
->method
== "PUT") {
986 auto document
= req
->json();
988 if (!isValidMetadataKind(kind
, false))
989 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
991 vector
<string
> vecMetadata
;
992 auto& metadata
= document
["metadata"];
993 if (!metadata
.is_array())
994 throw ApiException("metadata is not specified or not an array");
996 for (const auto& i
: metadata
.array_items()) {
998 throw ApiException("metadata must be strings");
999 vecMetadata
.push_back(i
.string_value());
1002 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
1003 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
1006 { "type", "Metadata" },
1008 { "metadata", metadata
}
1012 } else if (req
->method
== "DELETE") {
1013 if (!isValidMetadataKind(kind
, false))
1014 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1016 vector
<string
> md
; // an empty vector will do it
1017 if (!B
.setDomainMetadata(zonename
, kind
, md
))
1018 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1020 throw HttpMethodNotAllowedException();
1023 // Throws 404 if the key with inquireKeyId does not exist
1024 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1025 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1027 for(const auto& value
: keyset
) {
1028 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1034 throw HttpNotFoundException();
1038 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1039 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1041 bool inquireSingleKey
= inquireKeyId
>= 0;
1044 for(const auto& value
: keyset
) {
1045 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1050 switch (value
.second
.keyType
) {
1051 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1052 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1053 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1057 { "type", "Cryptokey" },
1058 { "id", (int)value
.second
.id
},
1059 { "active", value
.second
.active
},
1060 { "published", value
.second
.published
},
1061 { "keytype", keyType
},
1062 { "flags", (uint16_t)value
.first
.d_flags
},
1063 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1064 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1065 { "bits", value
.first
.getKey()->getBits() }
1068 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1070 for(const uint8_t keyid
: { DNSSECKeeper::DIGEST_SHA1
, DNSSECKeeper::DIGEST_SHA256
, DNSSECKeeper::DIGEST_GOST
, DNSSECKeeper::DIGEST_SHA384
})
1072 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1077 if (inquireSingleKey
) {
1078 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1085 if (inquireSingleKey
) {
1086 // we came here because we couldn't find the requested key.
1087 throw HttpNotFoundException();
1094 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1095 * It deletes a key from :zone_name specified by :cryptokey_id.
1097 * Case 1: the backend returns true on removal. This means the key is gone.
1098 * The server returns 204 No Content, no body.
1099 * Case 2: the backend returns false on removal. An error occurred.
1100 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1101 * Case 3: the key or zone does not exist.
1102 * The server returns 404 Not Found
1104 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1105 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1109 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1114 * This method adds a key to a zone by generate it or content parameter.
1117 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1118 * "keytype" : "ksk|zsk" <string>
1119 * "active" : "true|false" <value>
1120 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1121 * "bits" : number of bits <int>
1125 * Case 1: keytype isn't ksk|zsk
1126 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1127 * Case 2: 'bits' must be a positive integer value.
1128 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1129 * Case 3: The "algorithm" isn't supported
1130 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1131 * Case 4: Algorithm <= 10 and no bits were passed
1132 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1133 * Case 5: The wrong keysize was passed
1134 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1135 * Case 6: If the server cant guess the keysize
1136 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1137 * Case 7: The key-creation failed
1138 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1139 * Case 8: The key in content has the wrong format
1140 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1141 * Case 9: The wrong combination of fields is submitted
1142 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1143 * Case 10: No content and everything was fine
1144 * The server returns 201 Created and all public data about the new cryptokey
1145 * Case 11: With specified content
1146 * The server returns 201 Created and all public data about the added cryptokey
1149 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1150 auto document
= req
->json();
1151 string privatekey_fieldname
= "privatekey";
1152 auto privatekey
= document
["privatekey"];
1153 if (privatekey
.is_null()) {
1154 // Fallback to the old "content" behaviour
1155 privatekey
= document
["content"];
1156 privatekey_fieldname
= "content";
1158 bool active
= boolFromJson(document
, "active", false);
1159 bool published
= boolFromJson(document
, "published", true);
1162 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1164 } else if (stringFromJson(document
, "keytype") == "zsk") {
1167 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1170 int64_t insertedId
= -1;
1172 if (privatekey
.is_null()) {
1173 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1174 auto docbits
= document
["bits"];
1175 if (!docbits
.is_null()) {
1176 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1177 throw ApiException("'bits' must be a positive integer value");
1179 bits
= docbits
.int_value();
1182 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1183 auto providedAlgo
= document
["algorithm"];
1184 if (providedAlgo
.is_string()) {
1185 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1186 if (algorithm
== -1)
1187 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1188 } else if (providedAlgo
.is_number()) {
1189 algorithm
= providedAlgo
.int_value();
1190 } else if (!providedAlgo
.is_null()) {
1191 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1195 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
, published
)) {
1196 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1198 } catch (std::runtime_error
& error
) {
1199 throw ApiException(error
.what());
1202 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1203 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1204 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1205 DNSKEYRecordContent dkrc
;
1206 DNSSECPrivateKey dpk
;
1208 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1209 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1210 // TODO remove in 4.2.0
1211 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1212 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1221 catch (std::runtime_error
& error
) {
1222 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1224 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
, published
)) {
1225 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1227 } catch (std::runtime_error
& error
) {
1228 throw ApiException(error
.what());
1231 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1233 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1235 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1240 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1241 * It de/activates a key from :zone_name specified by :cryptokey_id.
1243 * Case 1: invalid JSON data
1244 * The server returns 400 Bad Request
1245 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1246 * The server returns 204 No Content
1247 * Case 3: the backend returns false on de/activation. An error occurred.
1248 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1250 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1251 //throws an exception if the Body is empty
1252 auto document
= req
->json();
1253 //throws an exception if the key does not exist or is not a bool
1254 bool active
= boolFromJson(document
, "active");
1255 bool published
= boolFromJson(document
, "published", true);
1257 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1258 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1262 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1263 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1269 if (!dk
->publishKey(zonename
, inquireKeyId
)) {
1270 resp
->setErrorResult("Could not publish Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1274 if (!dk
->unpublishKey(zonename
, inquireKeyId
)) {
1275 resp
->setErrorResult("Could not unpublish Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1286 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1287 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1288 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1290 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1291 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1294 DNSSECKeeper
dk(&B
);
1296 if (!B
.getDomainInfo(zonename
, di
)) {
1297 throw HttpNotFoundException();
1300 int inquireKeyId
= -1;
1301 if (req
->parameters
.count("key_id")) {
1302 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1303 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1306 if (req
->method
== "GET") {
1307 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1308 } else if (req
->method
== "DELETE") {
1309 if (inquireKeyId
== -1)
1310 throw HttpBadRequestException();
1311 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1312 } else if (req
->method
== "POST") {
1313 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1314 } else if (req
->method
== "PUT") {
1315 if (inquireKeyId
== -1)
1316 throw HttpBadRequestException();
1317 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1319 throw HttpMethodNotAllowedException(); //Returns method not allowed
1323 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1324 DNSResourceRecord rr
;
1325 vector
<string
> zonedata
;
1326 stringtok(zonedata
, zonestring
, "\r\n");
1328 ZoneParserTNG
zpt(zonedata
, zonename
);
1329 zpt
.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
1333 string comment
= "Imported via the API";
1336 while(zpt
.get(rr
, &comment
)) {
1337 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1339 if(rr
.qtype
.getCode() == QType::SOA
)
1341 validateGatheredRRType(rr
);
1343 new_records
.push_back(rr
);
1346 catch(std::exception
& ae
) {
1347 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1351 /** Throws ApiException if records which violate RRset constraints are present.
1352 * NOTE: sorts records in-place.
1354 * Constraints being checked:
1355 * *) no exact duplicates
1356 * *) no duplicates for QTypes that can only be present once per RRset
1357 * *) hostnames are hostnames
1359 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1360 sort(records
.begin(), records
.end(),
1361 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1362 /* we need _strict_ weak ordering */
1363 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1367 DNSResourceRecord previous
;
1368 for(const auto& rec
: records
) {
1369 if (previous
.qname
== rec
.qname
) {
1370 if (previous
.qtype
== rec
.qtype
) {
1371 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1372 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1374 if (previous
.content
== rec
.content
) {
1375 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1377 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1378 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1382 // Check if the DNSNames that should be hostnames, are hostnames
1384 checkHostnameCorrectness(rec
);
1385 } catch (const std::exception
& e
) {
1386 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1393 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1395 string contentFromDB
;
1396 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1397 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1398 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1402 if (!getTSIGHashEnum(algo
, the
)) {
1403 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1407 if (B64Decode(content
, b64out
) == -1) {
1408 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1412 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1413 Json::object tsigkey
= {
1414 { "name", keyname
.toStringNoDot() },
1415 { "id", apiZoneNameToId(keyname
) },
1416 { "algorithm", algo
.toStringNoDot() },
1418 { "type", "TSIGKey" }
1423 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1424 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1427 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1429 if (req
->method
== "GET") {
1430 vector
<struct TSIGKey
> keys
;
1432 if (!B
.getTSIGKeys(keys
)) {
1433 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1438 for(const auto &key
: keys
) {
1439 doc
.push_back(makeJSONTSIGKey(key
, false));
1442 } else if (req
->method
== "POST") {
1443 auto document
= req
->json();
1444 DNSName
keyname(stringFromJson(document
, "name"));
1445 DNSName
algo(stringFromJson(document
, "algorithm"));
1446 string content
= document
["key"].string_value();
1448 if (content
.empty()) {
1450 content
= makeTSIGKey(algo
);
1451 } catch (const PDNSException
& e
) {
1452 throw HttpBadRequestException(e
.reason
);
1456 // Will throw an ApiException or HttpConflictException on error
1457 checkTSIGKey(B
, keyname
, algo
, content
);
1459 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1460 throw HttpInternalServerErrorException("Unable to add TSIG key");
1464 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1466 throw HttpMethodNotAllowedException();
1470 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1472 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1476 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1477 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1482 tsk
.algorithm
= algo
;
1485 if (req
->method
== "GET") {
1486 resp
->setBody(makeJSONTSIGKey(tsk
));
1487 } else if (req
->method
== "PUT") {
1488 json11::Json document
;
1489 if (!req
->body
.empty()) {
1490 document
= req
->json();
1492 if (document
["name"].is_string()) {
1493 tsk
.name
= DNSName(document
["name"].string_value());
1495 if (document
["algorithm"].is_string()) {
1496 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1499 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1500 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1503 if (document
["key"].is_string()) {
1504 string new_content
= document
["key"].string_value();
1506 if (B64Decode(new_content
, decoded
) == -1) {
1507 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1509 tsk
.key
= new_content
;
1511 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1512 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1514 if (tsk
.name
!= keyname
) {
1515 // Remove the old key
1516 if (!B
.deleteTSIGKey(keyname
)) {
1517 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1520 resp
->setBody(makeJSONTSIGKey(tsk
));
1521 } else if (req
->method
== "DELETE") {
1522 if (!B
.deleteTSIGKey(keyname
)) {
1523 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1529 throw HttpMethodNotAllowedException();
1533 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1535 DNSSECKeeper
dk(&B
);
1536 if (req
->method
== "POST") {
1538 auto document
= req
->json();
1539 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1540 apiCheckNameAllowedCharacters(zonename
.toString());
1541 zonename
.makeUsLowerCase();
1543 bool exists
= B
.getDomainInfo(zonename
, di
);
1545 throw HttpConflictException();
1547 // validate 'kind' is set
1548 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1550 string zonestring
= document
["zone"].string_value();
1551 auto rrsets
= document
["rrsets"];
1552 if (rrsets
.is_array() && zonestring
!= "")
1553 throw ApiException("You cannot give rrsets AND zone data as text");
1555 auto nameservers
= document
["nameservers"];
1556 if (!nameservers
.is_null() && !nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1557 throw ApiException("Nameservers is not a list");
1559 string soa_edit_api_kind
;
1560 if (document
["soa_edit_api"].is_string()) {
1561 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1564 soa_edit_api_kind
= "DEFAULT";
1566 string soa_edit_kind
= document
["soa_edit"].string_value();
1568 // if records/comments are given, load and check them
1569 bool have_soa
= false;
1570 bool have_zone_ns
= false;
1571 vector
<DNSResourceRecord
> new_records
;
1572 vector
<Comment
> new_comments
;
1573 vector
<DNSResourceRecord
> new_ptrs
;
1575 if (rrsets
.is_array()) {
1576 for (const auto& rrset
: rrsets
.array_items()) {
1577 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1578 apiCheckQNameAllowedCharacters(qname
.toString());
1580 qtype
= stringFromJson(rrset
, "type");
1581 if (qtype
.getCode() == 0) {
1582 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1584 if (rrset
["records"].is_array()) {
1585 int ttl
= intFromJson(rrset
, "ttl");
1586 gatherRecords(B
, req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1588 if (rrset
["comments"].is_array()) {
1589 gatherComments(rrset
, qname
, qtype
, new_comments
);
1592 } else if (zonestring
!= "") {
1593 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1596 for(auto& rr
: new_records
) {
1597 rr
.qname
.makeUsLowerCase();
1598 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1599 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1600 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1602 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1604 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1606 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1607 have_zone_ns
= true;
1611 // synthesize RRs as needed
1612 DNSResourceRecord autorr
;
1613 autorr
.qname
= zonename
;
1615 autorr
.ttl
= ::arg().asNum("default-ttl");
1617 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1618 // synthesize a SOA record so the zone "really" exists
1619 string soa
= (boost::format("%s %s %ul")
1620 % ::arg()["default-soa-name"]
1621 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1622 % document
["serial"].int_value()
1625 fillSOAData(soa
, sd
); // fills out default values for us
1626 autorr
.qtype
= QType::SOA
;
1627 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1628 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1629 new_records
.push_back(autorr
);
1632 // create NS records if nameservers are given
1633 for (auto value
: nameservers
.array_items()) {
1634 string nameserver
= value
.string_value();
1635 if (nameserver
.empty())
1636 throw ApiException("Nameservers must be non-empty strings");
1637 if (!isCanonical(nameserver
))
1638 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1640 // ensure the name parses
1641 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1643 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1645 autorr
.qtype
= QType::NS
;
1646 new_records
.push_back(autorr
);
1648 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1652 checkNewRecords(new_records
);
1654 if (boolFromJson(document
, "dnssec", false)) {
1655 checkDefaultDNSSECAlgos();
1657 if(document
["nsec3param"].string_value().length() > 0) {
1658 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1659 string error_msg
= "";
1660 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1661 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1666 // no going back after this
1667 if(!B
.createDomain(zonename
))
1668 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1670 if(!B
.getDomainInfo(zonename
, di
))
1671 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1673 di
.backend
->startTransaction(zonename
, di
.id
);
1675 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1676 if (!soa_edit_api_kind
.empty()) {
1677 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1680 for(auto rr
: new_records
) {
1681 rr
.domain_id
= di
.id
;
1682 di
.backend
->feedRecord(rr
, DNSName());
1684 for(Comment
& c
: new_comments
) {
1685 c
.domain_id
= di
.id
;
1686 di
.backend
->feedComment(c
);
1689 updateDomainSettingsFromDocument(B
, di
, zonename
, document
, false);
1691 di
.backend
->commitTransaction();
1693 storeChangedPTRs(B
, new_ptrs
);
1695 fillZone(B
, zonename
, resp
, shouldDoRRSets(req
));
1700 if(req
->method
!= "GET")
1701 throw HttpMethodNotAllowedException();
1703 vector
<DomainInfo
> domains
;
1705 if (req
->getvars
.count("zone")) {
1706 string zone
= req
->getvars
["zone"];
1707 apiCheckNameAllowedCharacters(zone
);
1708 DNSName zonename
= apiNameToDNSName(zone
);
1709 zonename
.makeUsLowerCase();
1711 if (B
.getDomainInfo(zonename
, di
)) {
1712 domains
.push_back(di
);
1716 B
.getAllDomains(&domains
, true); // incl. disabled
1717 } catch(const PDNSException
&e
) {
1718 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + e
.reason
);
1722 bool with_dnssec
= true;
1723 if (req
->getvars
.count("dnssec")) {
1724 // can send ?dnssec=false to improve performance.
1725 string dnssec_flag
= req
->getvars
["dnssec"];
1726 if (dnssec_flag
== "false") {
1727 with_dnssec
= false;
1732 doc
.reserve(domains
.size());
1733 for(const DomainInfo
& di
: domains
) {
1734 doc
.push_back(getZoneInfo(di
, with_dnssec
? &dk
: nullptr));
1739 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1740 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1745 if (!B
.getDomainInfo(zonename
, di
)) {
1746 throw HttpNotFoundException();
1748 } catch(const PDNSException
&e
) {
1749 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1752 if(req
->method
== "PUT") {
1753 // update domain settings
1755 di
.backend
->startTransaction(zonename
, -1);
1756 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json(), false);
1757 di
.backend
->commitTransaction();
1760 resp
->status
= 204; // No Content, but indicate success
1763 else if(req
->method
== "DELETE") {
1765 if(!di
.backend
->deleteDomain(zonename
))
1766 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1769 DNSSECKeeper::clearCaches(zonename
);
1770 purgeAuthCaches(zonename
.toString() + "$");
1772 // empty body on success
1774 resp
->status
= 204; // No Content: declare that the zone is gone now
1776 } else if (req
->method
== "PATCH") {
1777 patchZone(B
, req
, resp
);
1779 } else if (req
->method
== "GET") {
1780 fillZone(B
, zonename
, resp
, shouldDoRRSets(req
));
1783 throw HttpMethodNotAllowedException();
1786 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1787 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1789 if(req
->method
!= "GET")
1790 throw HttpMethodNotAllowedException();
1796 if (!B
.getDomainInfo(zonename
, di
)) {
1797 throw HttpNotFoundException();
1800 DNSResourceRecord rr
;
1802 di
.backend
->list(zonename
, di
.id
);
1803 while(di
.backend
->get(rr
)) {
1804 if (!rr
.qtype
.getCode())
1805 continue; // skip empty non-terminals
1808 rr
.qname
.toString() << "\t" <<
1811 rr
.qtype
.getName() << "\t" <<
1812 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1816 if (req
->accept_json
) {
1817 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1819 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1820 resp
->body
= ss
.str();
1824 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1825 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1827 if(req
->method
!= "PUT")
1828 throw HttpMethodNotAllowedException();
1832 if (!B
.getDomainInfo(zonename
, di
)) {
1833 throw HttpNotFoundException();
1836 if(di
.masters
.empty())
1837 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1839 shuffle(di
.masters
.begin(), di
.masters
.end(), pdns::dns_random_engine());
1840 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1841 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1844 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1845 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1847 if(req
->method
!= "PUT")
1848 throw HttpMethodNotAllowedException();
1852 if (!B
.getDomainInfo(zonename
, di
)) {
1853 throw HttpNotFoundException();
1856 if(!Communicator
.notifyDomain(zonename
, &B
))
1857 throw ApiException("Failed to add to the queue - see server log");
1859 resp
->setSuccessResult("Notification queued");
1862 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1863 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1865 if(req
->method
!= "PUT")
1866 throw HttpMethodNotAllowedException();
1870 if (!B
.getDomainInfo(zonename
, di
)) {
1871 throw HttpNotFoundException();
1874 DNSSECKeeper
dk(&B
);
1876 if (!dk
.isSecuredZone(zonename
))
1877 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1879 if (di
.kind
== DomainInfo::Slave
)
1880 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1882 string error_msg
= "";
1884 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1885 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1887 resp
->setSuccessResult("Rectified");
1890 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1891 if (rr
.qtype
.getCode() == QType::A
) {
1893 if (!IpToU32(rr
.content
, &ip
)) {
1894 throw ApiException("PTR: Invalid IP address given");
1896 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1897 % ((ip
>> 24) & 0xff)
1898 % ((ip
>> 16) & 0xff)
1899 % ((ip
>> 8) & 0xff)
1902 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1903 ComboAddress
ca(rr
.content
);
1906 for (int octet
= 0; octet
< 16; ++octet
) {
1907 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1908 // this should be impossible: no byte should give more than two digits in hex format
1909 throw PDNSException("Formatting IPv6 address failed");
1911 ss
<< buf
[0] << '.' << buf
[1] << '.';
1913 string tmp
= ss
.str();
1914 tmp
.resize(tmp
.size()-1); // remove last dot
1915 // reverse and append arpa domain
1916 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1918 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1923 ptr
->disabled
= rr
.disabled
;
1924 ptr
->content
= rr
.qname
.toStringRootDot();
1927 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1928 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1930 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1931 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1933 string soa_edit_api_kind
;
1934 string soa_edit_kind
;
1935 bool soa_changed
= false;
1936 DNSResourceRecord soarr
;
1937 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1938 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1939 if (!soa_edit_api_kind
.empty()) {
1940 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1943 sd
.db
->startTransaction(sd
.qname
);
1944 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1945 sd
.db
->abortTransaction();
1946 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1950 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1953 sd
.db
->commitTransaction();
1954 purgeAuthCachesExact(rr
.qname
);
1958 static void patchZone(UeberBackend
& B
, HttpRequest
* req
, HttpResponse
* resp
) {
1962 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1963 if (!B
.getDomainInfo(zonename
, di
)) {
1964 throw HttpNotFoundException();
1967 vector
<DNSResourceRecord
> new_records
;
1968 vector
<Comment
> new_comments
;
1969 vector
<DNSResourceRecord
> new_ptrs
;
1971 Json document
= req
->json();
1973 auto rrsets
= document
["rrsets"];
1974 if (!rrsets
.is_array())
1975 throw ApiException("No rrsets given in update request");
1977 di
.backend
->startTransaction(zonename
);
1980 string soa_edit_api_kind
;
1981 string soa_edit_kind
;
1982 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1983 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1984 bool soa_edit_done
= false;
1986 set
<pair
<DNSName
, QType
>> seen
;
1988 for (const auto& rrset
: rrsets
.array_items()) {
1989 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1990 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1991 apiCheckQNameAllowedCharacters(qname
.toString());
1993 qtype
= stringFromJson(rrset
, "type");
1994 if (qtype
.getCode() == 0) {
1995 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1998 if(seen
.count({qname
, qtype
}))
2000 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
2002 seen
.insert({qname
, qtype
});
2004 if (changetype
== "DELETE") {
2005 // delete all matching qname/qtype RRs (and, implicitly comments).
2006 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
2007 throw ApiException("Hosting backend does not support editing records.");
2010 else if (changetype
== "REPLACE") {
2011 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
2012 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
2013 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
2015 bool replace_records
= rrset
["records"].is_array();
2016 bool replace_comments
= rrset
["comments"].is_array();
2018 if (!replace_records
&& !replace_comments
) {
2019 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
2022 new_records
.clear();
2023 new_comments
.clear();
2025 if (replace_records
) {
2026 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2027 int ttl
= intFromJson(rrset
, "ttl");
2028 // new_ptrs is merged.
2029 gatherRecords(B
, req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
2031 for(DNSResourceRecord
& rr
: new_records
) {
2032 rr
.domain_id
= di
.id
;
2033 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
2034 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
2037 checkNewRecords(new_records
);
2040 if (replace_comments
) {
2041 gatherComments(rrset
, qname
, qtype
, new_comments
);
2043 for(Comment
& c
: new_comments
) {
2044 c
.domain_id
= di
.id
;
2048 if (replace_records
) {
2049 bool ent_present
= false;
2050 bool dname_seen
= false, ns_seen
= false;
2052 di
.backend
->lookup(QType(QType::ANY
), qname
, di
.id
);
2053 DNSResourceRecord rr
;
2054 while (di
.backend
->get(rr
)) {
2055 if (rr
.qtype
.getCode() == QType::ENT
) {
2057 /* that's fine, we will override it */
2060 if (qtype
== QType::DNAME
|| rr
.qtype
== QType::DNAME
)
2062 if (qtype
== QType::NS
|| rr
.qtype
== QType::NS
)
2064 if (qtype
.getCode() != rr
.qtype
.getCode()
2065 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2066 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2068 // leave database handle in a consistent state
2069 while (di
.backend
->get(rr
))
2072 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2076 if (dname_seen
&& ns_seen
&& qname
!= zonename
) {
2077 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Cannot have both NS and DNAME except in zone apex");
2079 if (!new_records
.empty() && ent_present
) {
2081 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2082 throw ApiException("Hosting backend does not support editing records.");
2085 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2086 throw ApiException("Hosting backend does not support editing records.");
2089 if (replace_comments
) {
2090 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2091 throw ApiException("Hosting backend does not support editing comments.");
2096 throw ApiException("Changetype not understood");
2099 zone_disabled
= (!B
.getSOAUncached(zonename
, sd
));
2101 // edit SOA (if needed)
2102 if (!zone_disabled
&& !soa_edit_api_kind
.empty() && !soa_edit_done
) {
2103 DNSResourceRecord rr
;
2104 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2105 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2106 throw ApiException("Hosting backend does not support editing records.");
2110 // return old and new serials in headers
2111 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2112 fillSOAData(rr
.content
, sd
);
2113 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2117 di
.backend
->abortTransaction();
2122 DNSSECKeeper
dk(&B
);
2123 if (!zone_disabled
&& !dk
.isPresigned(zonename
)) {
2125 if (!di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
) && ::arg().mustDo("default-api-rectify")) {
2128 if (api_rectify
== "1") {
2131 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false)) {
2132 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2137 di
.backend
->commitTransaction();
2139 purgeAuthCaches(zonename
.toString() + "$");
2142 storeChangedPTRs(B
, new_ptrs
);
2145 resp
->status
= 204; // No Content, but indicate success
2149 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2150 if(req
->method
!= "GET")
2151 throw HttpMethodNotAllowedException();
2153 string q
= req
->getvars
["q"];
2154 string sMax
= req
->getvars
["max"];
2155 string sObjectType
= req
->getvars
["object_type"];
2160 // the following types of data can be searched for using the api
2161 enum class ObjectType
2170 throw ApiException("Query q can't be blank");
2172 maxEnts
= std::stoi(sMax
);
2174 throw ApiException("Maximum entries must be larger than 0");
2176 if (sObjectType
.empty())
2177 objectType
= ObjectType::ALL
;
2178 else if (sObjectType
== "all")
2179 objectType
= ObjectType::ALL
;
2180 else if (sObjectType
== "zone")
2181 objectType
= ObjectType::ZONE
;
2182 else if (sObjectType
== "record")
2183 objectType
= ObjectType::RECORD
;
2184 else if (sObjectType
== "comment")
2185 objectType
= ObjectType::COMMENT
;
2187 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2189 SimpleMatch
sm(q
,true);
2191 vector
<DomainInfo
> domains
;
2192 vector
<DNSResourceRecord
> result_rr
;
2193 vector
<Comment
> result_c
;
2194 map
<int,DomainInfo
> zoneIdZone
;
2195 map
<int,DomainInfo
>::iterator val
;
2198 B
.getAllDomains(&domains
, true);
2200 for(const DomainInfo di
: domains
)
2202 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2203 doc
.push_back(Json::object
{
2204 { "object_type", "zone" },
2205 { "zone_id", apiZoneNameToId(di
.zone
) },
2206 { "name", di
.zone
.toString() }
2210 zoneIdZone
[di
.id
] = di
; // populate cache
2213 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2215 for(const DNSResourceRecord
& rr
: result_rr
)
2217 if (!rr
.qtype
.getCode())
2218 continue; // skip empty non-terminals
2220 auto object
= Json::object
{
2221 { "object_type", "record" },
2222 { "name", rr
.qname
.toString() },
2223 { "type", rr
.qtype
.getName() },
2224 { "ttl", (double)rr
.ttl
},
2225 { "disabled", rr
.disabled
},
2226 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2228 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2229 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2230 object
["zone"] = val
->second
.zone
.toString();
2232 doc
.push_back(object
);
2236 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2238 for(const Comment
&c
: result_c
)
2240 auto object
= Json::object
{
2241 { "object_type", "comment" },
2242 { "name", c
.qname
.toString() },
2243 { "content", c
.content
}
2245 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2246 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2247 object
["zone"] = val
->second
.zone
.toString();
2249 doc
.push_back(object
);
2256 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2257 if(req
->method
!= "PUT")
2258 throw HttpMethodNotAllowedException();
2260 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2262 uint64_t count
= purgeAuthCachesExact(canon
);
2263 resp
->setBody(Json::object
{
2264 { "count", (int) count
},
2265 { "result", "Flushed cache." }
2269 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2271 resp
->headers
["Cache-Control"] = "max-age=86400";
2272 resp
->headers
["Content-Type"] = "text/css";
2275 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2276 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2277 ret
<<"a { color: #0959c2; }"<<endl
;
2278 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2279 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2280 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2281 ret
<<".row:after { clear: both; }"<<endl
;
2282 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2283 ret
<<".all { width: 100%; }"<<endl
;
2284 ret
<<".headl { width: 60%; }"<<endl
;
2285 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2286 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=);";
2287 ret
<<" width: 154px; height: 20px; }"<<endl
;
2288 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2289 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2290 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2291 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2292 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2293 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2294 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2295 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2296 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2297 ret
<<".resetring {float: right; }"<<endl
;
2298 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
;
2299 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2300 ret
<<".resizering {float: right;}"<<endl
;
2301 resp
->body
= ret
.str();
2305 void AuthWebServer::webThread()
2308 setThreadName("pdns/webserver");
2309 if(::arg().mustDo("api")) {
2310 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2311 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2312 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2313 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2314 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2315 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2316 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2317 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2318 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2319 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2320 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2321 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2322 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2323 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2324 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2325 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2326 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2327 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2328 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2330 if (::arg().mustDo("webserver")) {
2331 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2332 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2337 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;