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() :
65 d_start(time(nullptr)),
70 if(arg().mustDo("webserver") || arg().mustDo("api")) {
71 d_ws
= new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
72 d_ws
->setApiKey(arg()["api-key"]);
73 d_ws
->setPassword(arg()["webserver-password"]);
74 d_ws
->setLogLevel(arg()["webserver-loglevel"]);
77 acl
.toMasks(::arg()["webserver-allow-from"]);
80 d_ws
->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
86 void AuthWebServer::go()
89 std::thread
webT(std::bind(&AuthWebServer::webThread
, this));
91 std::thread
statT(std::bind(&AuthWebServer::statThread
, this));
95 void AuthWebServer::statThread()
98 setThreadName("pdns/statHelper");
100 d_queries
.submit(S
.read("udp-queries"));
101 d_cachehits
.submit(S
.read("packetcache-hit"));
102 d_cachemisses
.submit(S
.read("packetcache-miss"));
103 d_qcachehits
.submit(S
.read("query-cache-hit"));
104 d_qcachemisses
.submit(S
.read("query-cache-miss"));
109 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
114 static string
htmlescape(const string
&s
) {
116 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
137 static void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
141 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
143 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
148 ret
<<"<div class=\"panel\">";
149 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
150 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
151 ret
<<"<div class=ringmeta>";
152 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
153 ret
<<"<span class=resizering>Resize: ";
154 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
155 for(int i
=0;sizes
[i
];++i
) {
156 if(S
.getRingSize(ringname
)!=sizes
[i
])
157 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
159 ret
<<"("<<sizes
[i
]<<") ";
161 ret
<<"</span></div>";
163 ret
<<"<table class=\"data\">";
165 int total
=max(1,tot
);
166 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
167 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
170 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
172 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
;
174 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
175 ret
<<"</table></div>"<<endl
;
178 void AuthWebServer::printvars(ostringstream
&ret
)
180 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
182 vector
<string
>entries
=S
.getEntries();
183 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
184 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
187 ret
<<"</table></div>"<<endl
;
190 void AuthWebServer::printargs(ostringstream
&ret
)
192 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
194 vector
<string
>entries
=arg().list();
195 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
196 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
200 string
AuthWebServer::makePercentage(const double& val
)
202 return (boost::format("%.01f%%") % val
).str();
205 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
207 if(!req
->getvars
["resetring"].empty()) {
208 if (S
.ringExists(req
->getvars
["resetring"]))
209 S
.resetRing(req
->getvars
["resetring"]);
211 resp
->headers
["Location"] = req
->url
.path
;
214 if(!req
->getvars
["resizering"].empty()){
215 int size
=std::stoi(req
->getvars
["size"]);
216 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
217 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
219 resp
->headers
["Location"] = req
->url
.path
;
225 ret
<<"<!DOCTYPE html>"<<endl
;
226 ret
<<"<html><head>"<<endl
;
227 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
228 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
229 ret
<<"</head><body>"<<endl
;
231 ret
<<"<div class=\"row\">"<<endl
;
232 ret
<<"<div class=\"headl columns\">";
233 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
234 if(!arg()["config-name"].empty()) {
235 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
237 ret
<<"</a></div>"<<endl
;
238 ret
<<"<div class=\"headr columns\"></div></div>";
239 ret
<<"<div class=\"row\"><div class=\"all columns\">";
241 time_t passed
=time(0)-s_starttime
;
244 humanDuration(passed
)<<
247 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
248 (int)d_queries
.get1()<<", "<<
249 (int)d_queries
.get5()<<", "<<
250 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
253 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
254 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
255 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
256 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
257 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
260 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
261 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
262 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
263 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
264 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
267 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
268 (int)d_qcachemisses
.get1()<<", "<<
269 (int)d_qcachemisses
.get5()<<", "<<
270 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
273 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
274 if(req
->getvars
["ring"].empty()) {
275 auto entries
= S
.listRings();
276 for(const auto &i
: entries
) {
277 printtable(ret
, i
, S
.getRingTitle(i
));
281 if(arg().mustDo("webserver-print-arguments"))
284 else if(S
.ringExists(req
->getvars
["ring"]))
285 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
287 ret
<<"</div></div>"<<endl
;
288 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
289 ret
<<"</body></html>"<<endl
;
291 resp
->body
= ret
.str();
295 /** Helper to build a record content as needed. */
296 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
297 // noDot: for backend storage, pass true. for API users, pass false.
298 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
299 return drc
->getZoneRepresentation(noDot
);
302 /** "Normalize" record content for API consumers. */
303 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
304 return makeRecordContent(qtype
, content
, false);
307 /** "Normalize" record content for backend storage. */
308 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
309 return makeRecordContent(qtype
, content
, true);
312 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
* dk
) {
313 string zoneId
= apiZoneNameToId(di
.zone
);
314 vector
<string
> masters
;
315 masters
.reserve(di
.masters
.size());
316 for(const auto& m
: di
.masters
) {
317 masters
.push_back(m
.toStringWithPortExcept(53));
320 auto obj
= Json::object
{
321 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
323 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
324 { "name", di
.zone
.toString() },
325 { "kind", di
.getKindString() },
326 { "account", di
.account
},
327 { "masters", std::move(masters
) },
328 { "serial", (double)di
.serial
},
329 { "notified_serial", (double)di
.notified_serial
},
330 { "last_check", (double)di
.last_check
}
333 obj
["dnssec"] = dk
->isSecuredZone(di
.zone
);
334 obj
["edited_serial"] = (double)calculateEditSOA(di
.serial
, *dk
, di
.zone
);
339 static bool shouldDoRRSets(HttpRequest
* req
) {
340 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
342 if (req
->getvars
["rrsets"] == "false")
344 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
347 static void fillZone(UeberBackend
& B
, const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
349 if(!B
.getDomainInfo(zonename
, di
)) {
350 throw HttpNotFoundException();
354 Json::object doc
= getZoneInfo(di
, &dk
);
355 // extra stuff getZoneInfo doesn't do for us (more expensive)
357 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
358 doc
["soa_edit_api"] = soa_edit_api
;
360 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
361 doc
["soa_edit"] = soa_edit
;
363 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
364 doc
["nsec3param"] = nsec3param
;
366 bool nsec3narrowbool
= false;
367 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
368 if (nsec3narrow
== "1")
369 nsec3narrowbool
= true;
370 doc
["nsec3narrow"] = nsec3narrowbool
;
371 doc
["dnssec"] = dk
.isSecuredZone(zonename
);
374 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
375 doc
["api_rectify"] = (api_rectify
== "1");
378 vector
<string
> tsig_master
, tsig_slave
;
379 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
380 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
382 Json::array tsig_master_keys
;
383 for (const auto& keyname
: tsig_master
) {
384 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
386 doc
["master_tsig_key_ids"] = tsig_master_keys
;
388 Json::array tsig_slave_keys
;
389 for (const auto& keyname
: tsig_slave
) {
390 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
392 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
395 vector
<DNSResourceRecord
> records
;
396 vector
<Comment
> comments
;
398 // load all records + sort
400 DNSResourceRecord rr
;
401 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
402 while(di
.backend
->get(rr
)) {
403 if (!rr
.qtype
.getCode())
404 continue; // skip empty non-terminals
405 records
.push_back(rr
);
407 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
408 /* if you ever want to update this comparison function,
409 please be aware that you will also need to update the conditions in the code merging
410 the records and comments below */
411 if (a
.qname
== b
.qname
) {
412 return b
.qtype
< a
.qtype
;
414 return b
.qname
< a
.qname
;
418 // load all comments + sort
421 di
.backend
->listComments(di
.id
);
422 while(di
.backend
->getComment(comment
)) {
423 comments
.push_back(comment
);
425 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
426 /* if you ever want to update this comparison function,
427 please be aware that you will also need to update the conditions in the code merging
428 the records and comments below */
429 if (a
.qname
== b
.qname
) {
430 return b
.qtype
< a
.qtype
;
432 return b
.qname
< a
.qname
;
438 Json::array rrset_records
;
439 Json::array rrset_comments
;
440 DNSName current_qname
;
443 auto rit
= records
.begin();
444 auto cit
= comments
.begin();
446 while (rit
!= records
.end() || cit
!= comments
.end()) {
447 // 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
448 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
449 current_qname
= rit
->qname
;
450 current_qtype
= rit
->qtype
;
453 current_qname
= cit
->qname
;
454 current_qtype
= cit
->qtype
;
458 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
459 ttl
= min(ttl
, rit
->ttl
);
460 rrset_records
.push_back(Json::object
{
461 { "disabled", rit
->disabled
},
462 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
466 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
467 rrset_comments
.push_back(Json::object
{
468 { "modified_at", (double)cit
->modified_at
},
469 { "account", cit
->account
},
470 { "content", cit
->content
}
475 rrset
["name"] = current_qname
.toString();
476 rrset
["type"] = current_qtype
.getName();
477 rrset
["records"] = rrset_records
;
478 rrset
["comments"] = rrset_comments
;
479 rrset
["ttl"] = (double)ttl
;
480 rrsets
.push_back(rrset
);
482 rrset_records
.clear();
483 rrset_comments
.clear();
486 doc
["rrsets"] = rrsets
;
492 void productServerStatisticsFetch(map
<string
,string
>& out
)
494 vector
<string
> items
= S
.getEntries();
495 for(const string
& item
: items
) {
496 out
[item
] = std::to_string(S
.read(item
));
500 out
["uptime"] = std::to_string(time(0) - s_starttime
);
503 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
506 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
514 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
515 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
516 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
520 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
) {
521 DNSResourceRecord rr
;
527 validateGatheredRRType(rr
);
528 const auto& items
= container
["records"].array_items();
529 for(const auto& record
: items
) {
530 string content
= stringFromJson(record
, "content");
532 if(!record
["disabled"].is_null()) {
533 rr
.disabled
= boolFromJson(record
, "disabled");
536 // validate that the client sent something we can actually parse, and require that data to be dotted.
538 if (rr
.qtype
.getCode() != QType::AAAA
) {
539 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
540 if (!pdns_iequals(tmp
, content
)) {
541 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
544 struct in6_addr tmpbuf
;
545 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
546 throw std::runtime_error("Invalid IPv6 address");
549 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
551 catch(std::exception
& e
)
553 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
556 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
557 boolFromJson(record
, "set-ptr", false) == true) {
559 g_log
<<Logger::Warning
<<logprefix
<<"API call uses deprecated set-ptr feature, please remove it"<<endl
;
561 DNSResourceRecord ptr
;
564 // verify that there's a zone for the PTR
566 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
567 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
569 ptr
.domain_id
= sd
.domain_id
;
570 new_ptrs
.push_back(ptr
);
573 new_records
.push_back(rr
);
577 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
582 time_t now
= time(0);
583 for (auto comment
: container
["comments"].array_items()) {
584 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
585 c
.content
= stringFromJson(comment
, "content");
586 c
.account
= stringFromJson(comment
, "account");
587 new_comments
.push_back(c
);
591 static void checkDefaultDNSSECAlgos() {
592 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
593 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
594 int k_size
= arg().asNum("default-ksk-size");
595 int z_size
= arg().asNum("default-zsk-size");
597 // Sanity check DNSSEC parameters
598 if (::arg()["default-zsk-algorithm"] != "") {
600 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
601 else if (k_algo
<= 10 && k_size
== 0)
602 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
605 if (::arg()["default-zsk-algorithm"] != "") {
607 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
608 else if (z_algo
<= 10 && z_size
== 0)
609 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
613 static void throwUnableToSecure(const DNSName
& zonename
) {
614 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
615 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
618 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
, bool rectifyTransaction
=true) {
619 vector
<string
> zonemaster
;
620 bool shouldRectify
= false;
621 for(auto value
: document
["masters"].array_items()) {
622 string master
= value
.string_value();
624 throw ApiException("Master can not be an empty string");
626 ComboAddress
m(master
);
627 } catch (const PDNSException
&e
) {
628 throw ApiException("Master (" + master
+ ") is not an IP address: " + e
.reason
);
630 zonemaster
.push_back(master
);
633 if (zonemaster
.size()) {
634 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
636 if (document
["kind"].is_string()) {
637 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
639 if (document
["soa_edit_api"].is_string()) {
640 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
642 if (document
["soa_edit"].is_string()) {
643 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
646 bool api_rectify
= boolFromJson(document
, "api_rectify");
647 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
649 catch (const JsonException
&) {}
651 if (document
["account"].is_string()) {
652 di
.backend
->setAccount(zonename
, document
["account"].string_value());
656 bool dnssecInJSON
= false;
657 bool dnssecDocVal
= false;
660 dnssecDocVal
= boolFromJson(document
, "dnssec");
663 catch (const JsonException
&) {}
665 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
670 checkDefaultDNSSECAlgos();
672 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
673 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
674 int k_size
= arg().asNum("default-ksk-size");
675 int z_size
= arg().asNum("default-zsk-size");
679 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
680 throwUnableToSecure(zonename
);
686 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
687 throwUnableToSecure(zonename
);
691 // Used later for NSEC3PARAM
692 isDNSSECZone
= dk
.isSecuredZone(zonename
);
695 throwUnableToSecure(zonename
);
697 shouldRectify
= true;
700 // "dnssec": false in json
703 if (!dk
.unSecureZone(zonename
, error
, info
)) {
704 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
706 isDNSSECZone
= dk
.isSecuredZone(zonename
);
708 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
710 shouldRectify
= true;
715 if(document
["nsec3param"].string_value().length() > 0) {
716 shouldRectify
= true;
717 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
718 string error_msg
= "";
720 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
722 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
723 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
725 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
726 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
727 "' passed our basic sanity checks, but cannot be used with the current backend.");
731 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
734 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
735 if (api_rectify
.empty()) {
736 if (::arg().mustDo("default-api-rectify")) {
740 if (api_rectify
== "1") {
743 if (!dk
.rectifyZone(zonename
, error_msg
, info
, rectifyTransaction
)) {
744 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
749 string soa_edit_api_kind
;
750 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
751 if (!soa_edit_api_kind
.empty()) {
753 if (!B
.getSOAUncached(zonename
, sd
))
756 string soa_edit_kind
;
757 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
759 DNSResourceRecord rr
;
760 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
761 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
762 throw ApiException("Hosting backend does not support editing records.");
768 if (!document
["master_tsig_key_ids"].is_null()) {
769 vector
<string
> metadata
;
770 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
771 auto keyname(apiZoneIdToName(value
.string_value()));
774 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
775 if (keyAlgo
.empty() || keyContent
.empty()) {
776 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
778 metadata
.push_back(keyname
.toString());
780 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
781 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
784 if (!document
["slave_tsig_key_ids"].is_null()) {
785 vector
<string
> metadata
;
786 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
787 auto keyname(apiZoneIdToName(value
.string_value()));
790 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
791 if (keyAlgo
.empty() || keyContent
.empty()) {
792 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
794 metadata
.push_back(keyname
.toString());
796 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
797 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
802 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
803 static vector
<string
> builtinOptions
{
806 "ALLOW-DNSUPDATE-FROM",
807 "TSIG-ALLOW-DNSUPDATE",
809 "SOA-EDIT-DNSUPDATE",
813 "GSS-ALLOW-AXFR-PRINCIPAL",
814 "GSS-ACCEPTOR-PRINCIPAL",
825 "TSIG-ALLOW-DNSUPDATE"
828 // the following options do not allow modifications via API
829 static vector
<string
> protectedOptions
{
839 if (kind
.find("X-") == 0)
844 for (const string
& s
: builtinOptions
) {
846 for (const string
& s2
: protectedOptions
) {
847 if (!readonly
&& s
== s2
)
858 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
859 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
863 if (!B
.getDomainInfo(zonename
, di
)) {
864 throw HttpNotFoundException();
867 if (req
->method
== "GET") {
868 map
<string
, vector
<string
> > md
;
869 Json::array document
;
871 if (!B
.getAllDomainMetadata(zonename
, md
))
872 throw HttpNotFoundException();
874 for (const auto& i
: md
) {
876 for (string j
: i
.second
)
877 entries
.push_back(j
);
880 { "type", "Metadata" },
882 { "metadata", entries
}
885 document
.push_back(key
);
888 resp
->setBody(document
);
889 } else if (req
->method
== "POST") {
890 auto document
= req
->json();
892 vector
<string
> entries
;
895 kind
= stringFromJson(document
, "kind");
896 } catch (const JsonException
&) {
897 throw ApiException("kind is not specified or not a string");
900 if (!isValidMetadataKind(kind
, false))
901 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
903 vector
<string
> vecMetadata
;
905 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
906 throw ApiException("Could not retrieve metadata entries for domain '" +
907 zonename
.toString() + "'");
909 auto& metadata
= document
["metadata"];
910 if (!metadata
.is_array())
911 throw ApiException("metadata is not specified or not an array");
913 for (const auto& i
: metadata
.array_items()) {
915 throw ApiException("metadata must be strings");
916 else if (std::find(vecMetadata
.cbegin(),
918 i
.string_value()) == vecMetadata
.cend()) {
919 vecMetadata
.push_back(i
.string_value());
923 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
924 throw ApiException("Could not update metadata entries for domain '" +
925 zonename
.toString() + "'");
927 Json::array respMetadata
;
928 for (const string
& s
: vecMetadata
)
929 respMetadata
.push_back(s
);
932 { "type", "Metadata" },
933 { "kind", document
["kind"] },
934 { "metadata", respMetadata
}
940 throw HttpMethodNotAllowedException();
943 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
944 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
948 if (!B
.getDomainInfo(zonename
, di
)) {
949 throw HttpNotFoundException();
952 string kind
= req
->parameters
["kind"];
954 if (req
->method
== "GET") {
955 vector
<string
> metadata
;
956 Json::object document
;
959 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
960 throw HttpNotFoundException();
961 else if (!isValidMetadataKind(kind
, true))
962 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
964 document
["type"] = "Metadata";
965 document
["kind"] = kind
;
967 for (const string
& i
: metadata
)
968 entries
.push_back(i
);
970 document
["metadata"] = entries
;
971 resp
->setBody(document
);
972 } else if (req
->method
== "PUT") {
973 auto document
= req
->json();
975 if (!isValidMetadataKind(kind
, false))
976 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
978 vector
<string
> vecMetadata
;
979 auto& metadata
= document
["metadata"];
980 if (!metadata
.is_array())
981 throw ApiException("metadata is not specified or not an array");
983 for (const auto& i
: metadata
.array_items()) {
985 throw ApiException("metadata must be strings");
986 vecMetadata
.push_back(i
.string_value());
989 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
990 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
993 { "type", "Metadata" },
995 { "metadata", metadata
}
999 } else if (req
->method
== "DELETE") {
1000 if (!isValidMetadataKind(kind
, false))
1001 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1003 vector
<string
> md
; // an empty vector will do it
1004 if (!B
.setDomainMetadata(zonename
, kind
, md
))
1005 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1007 throw HttpMethodNotAllowedException();
1010 // Throws 404 if the key with inquireKeyId does not exist
1011 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1012 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1014 for(const auto& value
: keyset
) {
1015 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1021 throw HttpNotFoundException();
1025 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1026 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1028 bool inquireSingleKey
= inquireKeyId
>= 0;
1031 for(const auto& value
: keyset
) {
1032 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1037 switch (value
.second
.keyType
) {
1038 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1039 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1040 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1044 { "type", "Cryptokey" },
1045 { "id", (int)value
.second
.id
},
1046 { "active", value
.second
.active
},
1047 { "published", value
.second
.published
},
1048 { "keytype", keyType
},
1049 { "flags", (uint16_t)value
.first
.d_flags
},
1050 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1051 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1052 { "bits", value
.first
.getKey()->getBits() }
1055 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1057 for(const uint8_t keyid
: { DNSSECKeeper::DIGEST_SHA1
, DNSSECKeeper::DIGEST_SHA256
, DNSSECKeeper::DIGEST_GOST
, DNSSECKeeper::DIGEST_SHA384
})
1059 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1064 if (inquireSingleKey
) {
1065 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1072 if (inquireSingleKey
) {
1073 // we came here because we couldn't find the requested key.
1074 throw HttpNotFoundException();
1081 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1082 * It deletes a key from :zone_name specified by :cryptokey_id.
1084 * Case 1: the backend returns true on removal. This means the key is gone.
1085 * The server returns 204 No Content, no body.
1086 * Case 2: the backend returns false on removal. An error occurred.
1087 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1088 * Case 3: the key or zone does not exist.
1089 * The server returns 404 Not Found
1091 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1092 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1096 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1101 * This method adds a key to a zone by generate it or content parameter.
1104 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1105 * "keytype" : "ksk|zsk" <string>
1106 * "active" : "true|false" <value>
1107 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1108 * "bits" : number of bits <int>
1112 * Case 1: keytype isn't ksk|zsk
1113 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1114 * Case 2: 'bits' must be a positive integer value.
1115 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1116 * Case 3: The "algorithm" isn't supported
1117 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1118 * Case 4: Algorithm <= 10 and no bits were passed
1119 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1120 * Case 5: The wrong keysize was passed
1121 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1122 * Case 6: If the server cant guess the keysize
1123 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1124 * Case 7: The key-creation failed
1125 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1126 * Case 8: The key in content has the wrong format
1127 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1128 * Case 9: The wrong combination of fields is submitted
1129 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1130 * Case 10: No content and everything was fine
1131 * The server returns 201 Created and all public data about the new cryptokey
1132 * Case 11: With specified content
1133 * The server returns 201 Created and all public data about the added cryptokey
1136 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1137 auto document
= req
->json();
1138 string privatekey_fieldname
= "privatekey";
1139 auto privatekey
= document
["privatekey"];
1140 if (privatekey
.is_null()) {
1141 // Fallback to the old "content" behaviour
1142 privatekey
= document
["content"];
1143 privatekey_fieldname
= "content";
1145 bool active
= boolFromJson(document
, "active", false);
1146 bool published
= boolFromJson(document
, "published", true);
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
, published
)) {
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
, published
)) {
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");
1242 bool published
= boolFromJson(document
, "published", true);
1244 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1245 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1249 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1250 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1256 if (!dk
->publishKey(zonename
, inquireKeyId
)) {
1257 resp
->setErrorResult("Could not publish Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1261 if (!dk
->unpublishKey(zonename
, inquireKeyId
)) {
1262 resp
->setErrorResult("Could not unpublish Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1273 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1274 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1275 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1277 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1278 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1281 DNSSECKeeper
dk(&B
);
1283 if (!B
.getDomainInfo(zonename
, di
)) {
1284 throw HttpNotFoundException();
1287 int inquireKeyId
= -1;
1288 if (req
->parameters
.count("key_id")) {
1289 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1290 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1293 if (req
->method
== "GET") {
1294 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1295 } else if (req
->method
== "DELETE") {
1296 if (inquireKeyId
== -1)
1297 throw HttpBadRequestException();
1298 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1299 } else if (req
->method
== "POST") {
1300 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1301 } else if (req
->method
== "PUT") {
1302 if (inquireKeyId
== -1)
1303 throw HttpBadRequestException();
1304 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1306 throw HttpMethodNotAllowedException(); //Returns method not allowed
1310 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1311 DNSResourceRecord rr
;
1312 vector
<string
> zonedata
;
1313 stringtok(zonedata
, zonestring
, "\r\n");
1315 ZoneParserTNG
zpt(zonedata
, zonename
);
1316 zpt
.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
1320 string comment
= "Imported via the API";
1323 while(zpt
.get(rr
, &comment
)) {
1324 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1326 if(rr
.qtype
.getCode() == QType::SOA
)
1328 validateGatheredRRType(rr
);
1330 new_records
.push_back(rr
);
1333 catch(std::exception
& ae
) {
1334 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1338 /** Throws ApiException if records which violate RRset constraints are present.
1339 * NOTE: sorts records in-place.
1341 * Constraints being checked:
1342 * *) no exact duplicates
1343 * *) no duplicates for QTypes that can only be present once per RRset
1344 * *) hostnames are hostnames
1346 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1347 sort(records
.begin(), records
.end(),
1348 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1349 /* we need _strict_ weak ordering */
1350 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1354 DNSResourceRecord previous
;
1355 for(const auto& rec
: records
) {
1356 if (previous
.qname
== rec
.qname
) {
1357 if (previous
.qtype
== rec
.qtype
) {
1358 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1359 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1361 if (previous
.content
== rec
.content
) {
1362 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1364 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1365 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1369 // Check if the DNSNames that should be hostnames, are hostnames
1371 checkHostnameCorrectness(rec
);
1372 } catch (const std::exception
& e
) {
1373 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1380 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1382 string contentFromDB
;
1383 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1384 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1385 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1389 if (!getTSIGHashEnum(algo
, the
)) {
1390 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1394 if (B64Decode(content
, b64out
) == -1) {
1395 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1399 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1400 Json::object tsigkey
= {
1401 { "name", keyname
.toStringNoDot() },
1402 { "id", apiZoneNameToId(keyname
) },
1403 { "algorithm", algo
.toStringNoDot() },
1405 { "type", "TSIGKey" }
1410 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1411 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1414 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1416 if (req
->method
== "GET") {
1417 vector
<struct TSIGKey
> keys
;
1419 if (!B
.getTSIGKeys(keys
)) {
1420 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1425 for(const auto &key
: keys
) {
1426 doc
.push_back(makeJSONTSIGKey(key
, false));
1429 } else if (req
->method
== "POST") {
1430 auto document
= req
->json();
1431 DNSName
keyname(stringFromJson(document
, "name"));
1432 DNSName
algo(stringFromJson(document
, "algorithm"));
1433 string content
= document
["key"].string_value();
1435 if (content
.empty()) {
1437 content
= makeTSIGKey(algo
);
1438 } catch (const PDNSException
& e
) {
1439 throw HttpBadRequestException(e
.reason
);
1443 // Will throw an ApiException or HttpConflictException on error
1444 checkTSIGKey(B
, keyname
, algo
, content
);
1446 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1447 throw HttpInternalServerErrorException("Unable to add TSIG key");
1451 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1453 throw HttpMethodNotAllowedException();
1457 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1459 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1463 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1464 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1469 tsk
.algorithm
= algo
;
1472 if (req
->method
== "GET") {
1473 resp
->setBody(makeJSONTSIGKey(tsk
));
1474 } else if (req
->method
== "PUT") {
1475 json11::Json document
;
1476 if (!req
->body
.empty()) {
1477 document
= req
->json();
1479 if (document
["name"].is_string()) {
1480 tsk
.name
= DNSName(document
["name"].string_value());
1482 if (document
["algorithm"].is_string()) {
1483 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1486 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1487 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1490 if (document
["key"].is_string()) {
1491 string new_content
= document
["key"].string_value();
1493 if (B64Decode(new_content
, decoded
) == -1) {
1494 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1496 tsk
.key
= new_content
;
1498 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1499 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1501 if (tsk
.name
!= keyname
) {
1502 // Remove the old key
1503 if (!B
.deleteTSIGKey(keyname
)) {
1504 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1507 resp
->setBody(makeJSONTSIGKey(tsk
));
1508 } else if (req
->method
== "DELETE") {
1509 if (!B
.deleteTSIGKey(keyname
)) {
1510 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1516 throw HttpMethodNotAllowedException();
1520 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1522 DNSSECKeeper
dk(&B
);
1523 if (req
->method
== "POST") {
1525 auto document
= req
->json();
1526 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1527 apiCheckNameAllowedCharacters(zonename
.toString());
1528 zonename
.makeUsLowerCase();
1530 bool exists
= B
.getDomainInfo(zonename
, di
);
1532 throw HttpConflictException();
1534 // validate 'kind' is set
1535 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1537 string zonestring
= document
["zone"].string_value();
1538 auto rrsets
= document
["rrsets"];
1539 if (rrsets
.is_array() && zonestring
!= "")
1540 throw ApiException("You cannot give rrsets AND zone data as text");
1542 auto nameservers
= document
["nameservers"];
1543 if (!nameservers
.is_null() && !nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1544 throw ApiException("Nameservers is not a list");
1546 string soa_edit_api_kind
;
1547 if (document
["soa_edit_api"].is_string()) {
1548 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1551 soa_edit_api_kind
= "DEFAULT";
1553 string soa_edit_kind
= document
["soa_edit"].string_value();
1555 // if records/comments are given, load and check them
1556 bool have_soa
= false;
1557 bool have_zone_ns
= false;
1558 vector
<DNSResourceRecord
> new_records
;
1559 vector
<Comment
> new_comments
;
1560 vector
<DNSResourceRecord
> new_ptrs
;
1562 if (rrsets
.is_array()) {
1563 for (const auto& rrset
: rrsets
.array_items()) {
1564 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1565 apiCheckQNameAllowedCharacters(qname
.toString());
1567 qtype
= stringFromJson(rrset
, "type");
1568 if (qtype
.getCode() == 0) {
1569 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1571 if (rrset
["records"].is_array()) {
1572 int ttl
= intFromJson(rrset
, "ttl");
1573 gatherRecords(B
, req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1575 if (rrset
["comments"].is_array()) {
1576 gatherComments(rrset
, qname
, qtype
, new_comments
);
1579 } else if (zonestring
!= "") {
1580 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1583 for(auto& rr
: new_records
) {
1584 rr
.qname
.makeUsLowerCase();
1585 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1586 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1587 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1589 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1591 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1593 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1594 have_zone_ns
= true;
1598 // synthesize RRs as needed
1599 DNSResourceRecord autorr
;
1600 autorr
.qname
= zonename
;
1602 autorr
.ttl
= ::arg().asNum("default-ttl");
1604 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1605 // synthesize a SOA record so the zone "really" exists
1606 string soa
= (boost::format("%s %s %ul")
1607 % ::arg()["default-soa-name"]
1608 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1609 % document
["serial"].int_value()
1612 fillSOAData(soa
, sd
); // fills out default values for us
1613 autorr
.qtype
= QType::SOA
;
1614 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1615 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1616 new_records
.push_back(autorr
);
1619 // create NS records if nameservers are given
1620 for (auto value
: nameservers
.array_items()) {
1621 string nameserver
= value
.string_value();
1622 if (nameserver
.empty())
1623 throw ApiException("Nameservers must be non-empty strings");
1624 if (!isCanonical(nameserver
))
1625 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1627 // ensure the name parses
1628 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1630 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1632 autorr
.qtype
= QType::NS
;
1633 new_records
.push_back(autorr
);
1635 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1639 checkNewRecords(new_records
);
1641 if (boolFromJson(document
, "dnssec", false)) {
1642 checkDefaultDNSSECAlgos();
1644 if(document
["nsec3param"].string_value().length() > 0) {
1645 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1646 string error_msg
= "";
1647 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1648 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1653 // no going back after this
1654 if(!B
.createDomain(zonename
))
1655 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1657 if(!B
.getDomainInfo(zonename
, di
))
1658 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1660 di
.backend
->startTransaction(zonename
, di
.id
);
1662 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1663 if (!soa_edit_api_kind
.empty()) {
1664 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1667 for(auto rr
: new_records
) {
1668 rr
.domain_id
= di
.id
;
1669 di
.backend
->feedRecord(rr
, DNSName());
1671 for(Comment
& c
: new_comments
) {
1672 c
.domain_id
= di
.id
;
1673 di
.backend
->feedComment(c
);
1676 updateDomainSettingsFromDocument(B
, di
, zonename
, document
, false);
1678 di
.backend
->commitTransaction();
1680 storeChangedPTRs(B
, new_ptrs
);
1682 fillZone(B
, zonename
, resp
, shouldDoRRSets(req
));
1687 if(req
->method
!= "GET")
1688 throw HttpMethodNotAllowedException();
1690 vector
<DomainInfo
> domains
;
1692 if (req
->getvars
.count("zone")) {
1693 string zone
= req
->getvars
["zone"];
1694 apiCheckNameAllowedCharacters(zone
);
1695 DNSName zonename
= apiNameToDNSName(zone
);
1696 zonename
.makeUsLowerCase();
1698 if (B
.getDomainInfo(zonename
, di
)) {
1699 domains
.push_back(di
);
1703 B
.getAllDomains(&domains
, true); // incl. disabled
1704 } catch(const PDNSException
&e
) {
1705 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + e
.reason
);
1709 bool with_dnssec
= true;
1710 if (req
->getvars
.count("dnssec")) {
1711 // can send ?dnssec=false to improve performance.
1712 string dnssec_flag
= req
->getvars
["dnssec"];
1713 if (dnssec_flag
== "false") {
1714 with_dnssec
= false;
1719 doc
.reserve(domains
.size());
1720 for(const DomainInfo
& di
: domains
) {
1721 doc
.push_back(getZoneInfo(di
, with_dnssec
? &dk
: nullptr));
1726 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1727 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1732 if (!B
.getDomainInfo(zonename
, di
)) {
1733 throw HttpNotFoundException();
1735 } catch(const PDNSException
&e
) {
1736 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1739 if(req
->method
== "PUT") {
1740 // update domain settings
1742 di
.backend
->startTransaction(zonename
, -1);
1743 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json(), false);
1744 di
.backend
->commitTransaction();
1747 resp
->status
= 204; // No Content, but indicate success
1750 else if(req
->method
== "DELETE") {
1752 if(!di
.backend
->deleteDomain(zonename
))
1753 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1756 DNSSECKeeper::clearCaches(zonename
);
1757 purgeAuthCaches(zonename
.toString() + "$");
1759 // empty body on success
1761 resp
->status
= 204; // No Content: declare that the zone is gone now
1763 } else if (req
->method
== "PATCH") {
1764 patchZone(B
, req
, resp
);
1766 } else if (req
->method
== "GET") {
1767 fillZone(B
, zonename
, resp
, shouldDoRRSets(req
));
1770 throw HttpMethodNotAllowedException();
1773 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1774 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1776 if(req
->method
!= "GET")
1777 throw HttpMethodNotAllowedException();
1783 if (!B
.getDomainInfo(zonename
, di
)) {
1784 throw HttpNotFoundException();
1787 DNSResourceRecord rr
;
1789 di
.backend
->list(zonename
, di
.id
);
1790 while(di
.backend
->get(rr
)) {
1791 if (!rr
.qtype
.getCode())
1792 continue; // skip empty non-terminals
1795 rr
.qname
.toString() << "\t" <<
1798 rr
.qtype
.getName() << "\t" <<
1799 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1803 if (req
->accept_json
) {
1804 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1806 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1807 resp
->body
= ss
.str();
1811 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1812 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1814 if(req
->method
!= "PUT")
1815 throw HttpMethodNotAllowedException();
1819 if (!B
.getDomainInfo(zonename
, di
)) {
1820 throw HttpNotFoundException();
1823 if(di
.masters
.empty())
1824 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1826 shuffle(di
.masters
.begin(), di
.masters
.end(), pdns::dns_random_engine());
1827 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1828 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1831 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1832 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1834 if(req
->method
!= "PUT")
1835 throw HttpMethodNotAllowedException();
1839 if (!B
.getDomainInfo(zonename
, di
)) {
1840 throw HttpNotFoundException();
1843 if(!Communicator
.notifyDomain(zonename
, &B
))
1844 throw ApiException("Failed to add to the queue - see server log");
1846 resp
->setSuccessResult("Notification queued");
1849 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1850 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1852 if(req
->method
!= "PUT")
1853 throw HttpMethodNotAllowedException();
1857 if (!B
.getDomainInfo(zonename
, di
)) {
1858 throw HttpNotFoundException();
1861 DNSSECKeeper
dk(&B
);
1863 if (!dk
.isSecuredZone(zonename
))
1864 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1866 if (di
.kind
== DomainInfo::Slave
)
1867 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1869 string error_msg
= "";
1871 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1872 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1874 resp
->setSuccessResult("Rectified");
1877 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1878 if (rr
.qtype
.getCode() == QType::A
) {
1880 if (!IpToU32(rr
.content
, &ip
)) {
1881 throw ApiException("PTR: Invalid IP address given");
1883 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1884 % ((ip
>> 24) & 0xff)
1885 % ((ip
>> 16) & 0xff)
1886 % ((ip
>> 8) & 0xff)
1889 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1890 ComboAddress
ca(rr
.content
);
1893 for (int octet
= 0; octet
< 16; ++octet
) {
1894 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1895 // this should be impossible: no byte should give more than two digits in hex format
1896 throw PDNSException("Formatting IPv6 address failed");
1898 ss
<< buf
[0] << '.' << buf
[1] << '.';
1900 string tmp
= ss
.str();
1901 tmp
.resize(tmp
.size()-1); // remove last dot
1902 // reverse and append arpa domain
1903 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1905 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1910 ptr
->disabled
= rr
.disabled
;
1911 ptr
->content
= rr
.qname
.toStringRootDot();
1914 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1915 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1917 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1918 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1920 string soa_edit_api_kind
;
1921 string soa_edit_kind
;
1922 bool soa_changed
= false;
1923 DNSResourceRecord soarr
;
1924 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1925 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1926 if (!soa_edit_api_kind
.empty()) {
1927 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1930 sd
.db
->startTransaction(sd
.qname
);
1931 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1932 sd
.db
->abortTransaction();
1933 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1937 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1940 sd
.db
->commitTransaction();
1941 purgeAuthCachesExact(rr
.qname
);
1945 static void patchZone(UeberBackend
& B
, HttpRequest
* req
, HttpResponse
* resp
) {
1949 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1950 if (!B
.getDomainInfo(zonename
, di
)) {
1951 throw HttpNotFoundException();
1954 vector
<DNSResourceRecord
> new_records
;
1955 vector
<Comment
> new_comments
;
1956 vector
<DNSResourceRecord
> new_ptrs
;
1958 Json document
= req
->json();
1960 auto rrsets
= document
["rrsets"];
1961 if (!rrsets
.is_array())
1962 throw ApiException("No rrsets given in update request");
1964 di
.backend
->startTransaction(zonename
);
1967 string soa_edit_api_kind
;
1968 string soa_edit_kind
;
1969 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1970 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1971 bool soa_edit_done
= false;
1973 set
<pair
<DNSName
, QType
>> seen
;
1975 for (const auto& rrset
: rrsets
.array_items()) {
1976 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1977 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1978 apiCheckQNameAllowedCharacters(qname
.toString());
1980 qtype
= stringFromJson(rrset
, "type");
1981 if (qtype
.getCode() == 0) {
1982 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1985 if(seen
.count({qname
, qtype
}))
1987 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1989 seen
.insert({qname
, qtype
});
1991 if (changetype
== "DELETE") {
1992 // delete all matching qname/qtype RRs (and, implicitly comments).
1993 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1994 throw ApiException("Hosting backend does not support editing records.");
1997 else if (changetype
== "REPLACE") {
1998 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1999 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
2000 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
2002 bool replace_records
= rrset
["records"].is_array();
2003 bool replace_comments
= rrset
["comments"].is_array();
2005 if (!replace_records
&& !replace_comments
) {
2006 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
2009 new_records
.clear();
2010 new_comments
.clear();
2012 if (replace_records
) {
2013 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2014 int ttl
= intFromJson(rrset
, "ttl");
2015 // new_ptrs is merged.
2016 gatherRecords(B
, req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
2018 for(DNSResourceRecord
& rr
: new_records
) {
2019 rr
.domain_id
= di
.id
;
2020 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
2021 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
2024 checkNewRecords(new_records
);
2027 if (replace_comments
) {
2028 gatherComments(rrset
, qname
, qtype
, new_comments
);
2030 for(Comment
& c
: new_comments
) {
2031 c
.domain_id
= di
.id
;
2035 if (replace_records
) {
2036 bool ent_present
= false;
2037 bool dname_seen
= false, ns_seen
= false;
2039 di
.backend
->lookup(QType(QType::ANY
), qname
, di
.id
);
2040 DNSResourceRecord rr
;
2041 while (di
.backend
->get(rr
)) {
2042 if (rr
.qtype
.getCode() == QType::ENT
) {
2044 /* that's fine, we will override it */
2047 if (qtype
== QType::DNAME
|| rr
.qtype
== QType::DNAME
)
2049 if (qtype
== QType::NS
|| rr
.qtype
== QType::NS
)
2051 if (qtype
.getCode() != rr
.qtype
.getCode()
2052 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2053 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2055 // leave database handle in a consistent state
2056 while (di
.backend
->get(rr
))
2059 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2063 if (dname_seen
&& ns_seen
&& qname
!= zonename
) {
2064 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Cannot have both NS and DNAME except in zone apex");
2066 if (!new_records
.empty() && ent_present
) {
2068 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2069 throw ApiException("Hosting backend does not support editing records.");
2072 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2073 throw ApiException("Hosting backend does not support editing records.");
2076 if (replace_comments
) {
2077 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2078 throw ApiException("Hosting backend does not support editing comments.");
2083 throw ApiException("Changetype not understood");
2086 zone_disabled
= (!B
.getSOAUncached(zonename
, sd
));
2088 // edit SOA (if needed)
2089 if (!zone_disabled
&& !soa_edit_api_kind
.empty() && !soa_edit_done
) {
2090 DNSResourceRecord rr
;
2091 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2092 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2093 throw ApiException("Hosting backend does not support editing records.");
2097 // return old and new serials in headers
2098 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2099 fillSOAData(rr
.content
, sd
);
2100 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2104 di
.backend
->abortTransaction();
2109 DNSSECKeeper
dk(&B
);
2110 if (!zone_disabled
&& !dk
.isPresigned(zonename
)) {
2112 if (!di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
) && ::arg().mustDo("default-api-rectify")) {
2115 if (api_rectify
== "1") {
2118 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false)) {
2119 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2124 di
.backend
->commitTransaction();
2126 purgeAuthCaches(zonename
.toString() + "$");
2129 storeChangedPTRs(B
, new_ptrs
);
2132 resp
->status
= 204; // No Content, but indicate success
2136 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2137 if(req
->method
!= "GET")
2138 throw HttpMethodNotAllowedException();
2140 string q
= req
->getvars
["q"];
2141 string sMax
= req
->getvars
["max"];
2142 string sObjectType
= req
->getvars
["object_type"];
2147 // the following types of data can be searched for using the api
2148 enum class ObjectType
2157 throw ApiException("Query q can't be blank");
2159 maxEnts
= std::stoi(sMax
);
2161 throw ApiException("Maximum entries must be larger than 0");
2163 if (sObjectType
.empty())
2164 objectType
= ObjectType::ALL
;
2165 else if (sObjectType
== "all")
2166 objectType
= ObjectType::ALL
;
2167 else if (sObjectType
== "zone")
2168 objectType
= ObjectType::ZONE
;
2169 else if (sObjectType
== "record")
2170 objectType
= ObjectType::RECORD
;
2171 else if (sObjectType
== "comment")
2172 objectType
= ObjectType::COMMENT
;
2174 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2176 SimpleMatch
sm(q
,true);
2178 vector
<DomainInfo
> domains
;
2179 vector
<DNSResourceRecord
> result_rr
;
2180 vector
<Comment
> result_c
;
2181 map
<int,DomainInfo
> zoneIdZone
;
2182 map
<int,DomainInfo
>::iterator val
;
2185 B
.getAllDomains(&domains
, true);
2187 for(const DomainInfo
& di
: domains
)
2189 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2190 doc
.push_back(Json::object
{
2191 { "object_type", "zone" },
2192 { "zone_id", apiZoneNameToId(di
.zone
) },
2193 { "name", di
.zone
.toString() }
2197 zoneIdZone
[di
.id
] = di
; // populate cache
2200 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2202 for(const DNSResourceRecord
& rr
: result_rr
)
2204 if (!rr
.qtype
.getCode())
2205 continue; // skip empty non-terminals
2207 auto object
= Json::object
{
2208 { "object_type", "record" },
2209 { "name", rr
.qname
.toString() },
2210 { "type", rr
.qtype
.getName() },
2211 { "ttl", (double)rr
.ttl
},
2212 { "disabled", rr
.disabled
},
2213 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2215 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2216 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2217 object
["zone"] = val
->second
.zone
.toString();
2219 doc
.push_back(object
);
2223 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2225 for(const Comment
&c
: result_c
)
2227 auto object
= Json::object
{
2228 { "object_type", "comment" },
2229 { "name", c
.qname
.toString() },
2230 { "content", c
.content
}
2232 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2233 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2234 object
["zone"] = val
->second
.zone
.toString();
2236 doc
.push_back(object
);
2243 static void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2244 if(req
->method
!= "PUT")
2245 throw HttpMethodNotAllowedException();
2247 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2249 uint64_t count
= purgeAuthCachesExact(canon
);
2250 resp
->setBody(Json::object
{
2251 { "count", (int) count
},
2252 { "result", "Flushed cache." }
2256 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2258 resp
->headers
["Cache-Control"] = "max-age=86400";
2259 resp
->headers
["Content-Type"] = "text/css";
2262 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2263 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2264 ret
<<"a { color: #0959c2; }"<<endl
;
2265 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2266 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2267 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2268 ret
<<".row:after { clear: both; }"<<endl
;
2269 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2270 ret
<<".all { width: 100%; }"<<endl
;
2271 ret
<<".headl { width: 60%; }"<<endl
;
2272 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2273 ret
<<"background-image: url(data:image/png;base64,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);";
2274 ret
<<" width: 154px; height: 20px; }"<<endl
;
2275 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2276 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2277 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2278 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2279 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2280 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2281 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2282 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2283 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2284 ret
<<".resetring {float: right; }"<<endl
;
2285 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
;
2286 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2287 ret
<<".resizering {float: right;}"<<endl
;
2288 resp
->body
= ret
.str();
2292 void AuthWebServer::webThread()
2295 setThreadName("pdns/webserver");
2296 if(::arg().mustDo("api")) {
2297 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2298 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2299 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2300 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2301 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2302 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2303 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2304 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2305 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2306 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2307 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2308 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2309 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2310 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2311 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2312 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2313 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2314 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2315 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2317 if (::arg().mustDo("webserver")) {
2318 d_ws
->registerWebHandler("/style.css", std::bind(&AuthWebServer::cssfunction
, this, std::placeholders::_1
, std::placeholders::_2
));
2319 d_ws
->registerWebHandler("/", std::bind(&AuthWebServer::indexfunction
, this, std::placeholders::_1
, std::placeholders::_2
));
2324 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;