2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 #include "dynlistener.hh"
29 #include "webserver.hh"
34 #include "arguments.hh"
37 #include "ueberbackend.hh"
38 #include <boost/format.hpp>
40 #include "namespaces.hh"
43 #include "dnsseckeeper.hh"
45 #include "zoneparser-tng.hh"
46 #include "common_startup.hh"
47 #include "auth-caches.hh"
48 #include "threadname.hh"
49 #include "tsigutils.hh"
55 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
);
56 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
);
57 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
);
59 // QTypes that MUST NOT have multiple records of the same type in a given RRset.
60 static const std::set
<uint16_t> onlyOneEntryTypes
= { QType::CNAME
, QType::DNAME
, QType::SOA
};
61 // QTypes that MUST NOT be used with any other QType on the same name.
62 static const std::set
<uint16_t> exclusiveEntryTypes
= { QType::CNAME
, QType::DNAME
};
64 AuthWebServer::AuthWebServer() :
66 d_start(time(nullptr)),
71 if(arg().mustDo("webserver") || arg().mustDo("api")) {
72 d_ws
= new WebServer(arg()["webserver-address"], arg().asNum("webserver-port"));
73 d_ws
->setApiKey(arg()["api-key"]);
74 d_ws
->setPassword(arg()["webserver-password"]);
77 acl
.toMasks(::arg()["webserver-allow-from"]);
84 void AuthWebServer::go()
87 pthread_create(&d_tid
, 0, webThreadHelper
, this);
88 pthread_create(&d_tid
, 0, statThreadHelper
, this);
91 void AuthWebServer::statThread()
94 setThreadName("pdns/statHelper");
96 d_queries
.submit(S
.read("udp-queries"));
97 d_cachehits
.submit(S
.read("packetcache-hit"));
98 d_cachemisses
.submit(S
.read("packetcache-miss"));
99 d_qcachehits
.submit(S
.read("query-cache-hit"));
100 d_qcachemisses
.submit(S
.read("query-cache-miss"));
105 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
110 void *AuthWebServer::statThreadHelper(void *p
)
112 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
114 return 0; // never reached
117 void *AuthWebServer::webThreadHelper(void *p
)
119 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
121 return 0; // never reached
124 static string
htmlescape(const string
&s
) {
126 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
147 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
151 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
153 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
158 ret
<<"<div class=\"panel\">";
159 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
160 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
161 ret
<<"<div class=ringmeta>";
162 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
163 ret
<<"<span class=resizering>Resize: ";
164 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
165 for(int i
=0;sizes
[i
];++i
) {
166 if(S
.getRingSize(ringname
)!=sizes
[i
])
167 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
169 ret
<<"("<<sizes
[i
]<<") ";
171 ret
<<"</span></div>";
173 ret
<<"<table class=\"data\">";
175 int total
=max(1,tot
);
176 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
177 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
180 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
182 ret
<<"<tr><td><b>Rest:</b></td><td><b>"<<tot
-printed
<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot
-printed
)*100.0/total
)<<"</b></td>"<<endl
;
184 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
185 ret
<<"</table></div>"<<endl
;
188 void AuthWebServer::printvars(ostringstream
&ret
)
190 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
192 vector
<string
>entries
=S
.getEntries();
193 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
194 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
197 ret
<<"</table></div>"<<endl
;
200 void AuthWebServer::printargs(ostringstream
&ret
)
202 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
204 vector
<string
>entries
=arg().list();
205 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
206 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
210 string
AuthWebServer::makePercentage(const double& val
)
212 return (boost::format("%.01f%%") % val
).str();
215 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
217 if(!req
->getvars
["resetring"].empty()) {
218 if (S
.ringExists(req
->getvars
["resetring"]))
219 S
.resetRing(req
->getvars
["resetring"]);
221 resp
->headers
["Location"] = req
->url
.path
;
224 if(!req
->getvars
["resizering"].empty()){
225 int size
=std::stoi(req
->getvars
["size"]);
226 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
227 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
229 resp
->headers
["Location"] = req
->url
.path
;
235 ret
<<"<!DOCTYPE html>"<<endl
;
236 ret
<<"<html><head>"<<endl
;
237 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
238 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
239 ret
<<"</head><body>"<<endl
;
241 ret
<<"<div class=\"row\">"<<endl
;
242 ret
<<"<div class=\"headl columns\">";
243 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
244 if(!arg()["config-name"].empty()) {
245 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
247 ret
<<"</a></div>"<<endl
;
248 ret
<<"<div class=\"headr columns\"></div></div>";
249 ret
<<"<div class=\"row\"><div class=\"all columns\">";
251 time_t passed
=time(0)-s_starttime
;
254 humanDuration(passed
)<<
257 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
258 (int)d_queries
.get1()<<", "<<
259 (int)d_queries
.get5()<<", "<<
260 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
263 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
264 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
265 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
266 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
267 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
270 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
271 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
272 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
273 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
274 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
277 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
278 (int)d_qcachemisses
.get1()<<", "<<
279 (int)d_qcachemisses
.get5()<<", "<<
280 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
283 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
284 if(req
->getvars
["ring"].empty()) {
285 vector
<string
>entries
=S
.listRings();
286 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
)
287 printtable(ret
,*i
,S
.getRingTitle(*i
));
290 if(arg().mustDo("webserver-print-arguments"))
293 else if(S
.ringExists(req
->getvars
["ring"]))
294 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
296 ret
<<"</div></div>"<<endl
;
297 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"http://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
298 ret
<<"</body></html>"<<endl
;
300 resp
->body
= ret
.str();
304 /** Helper to build a record content as needed. */
305 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
306 // noDot: for backend storage, pass true. for API users, pass false.
307 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
308 return drc
->getZoneRepresentation(noDot
);
311 /** "Normalize" record content for API consumers. */
312 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
313 return makeRecordContent(qtype
, content
, false);
316 /** "Normalize" record content for backend storage. */
317 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
318 return makeRecordContent(qtype
, content
, true);
321 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
*dk
) {
322 string zoneId
= apiZoneNameToId(di
.zone
);
323 vector
<string
> masters
;
324 for(const auto& m
: di
.masters
)
325 masters
.push_back(m
.toStringWithPortExcept(53));
327 return Json::object
{
328 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
330 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
331 { "name", di
.zone
.toString() },
332 { "kind", di
.getKindString() },
333 { "dnssec", dk
->isSecuredZone(di
.zone
) },
334 { "account", di
.account
},
335 { "masters", masters
},
336 { "serial", (double)di
.serial
},
337 { "notified_serial", (double)di
.notified_serial
},
338 { "last_check", (double)di
.last_check
}
342 static bool shouldDoRRSets(HttpRequest
* req
) {
343 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
345 if (req
->getvars
["rrsets"] == "false")
347 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
350 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
353 if(!B
.getDomainInfo(zonename
, di
)) {
354 throw HttpNotFoundException();
358 Json::object doc
= getZoneInfo(di
, &dk
);
359 // extra stuff getZoneInfo doesn't do for us (more expensive)
361 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
362 doc
["soa_edit_api"] = soa_edit_api
;
364 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
365 doc
["soa_edit"] = soa_edit
;
367 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
368 doc
["nsec3param"] = nsec3param
;
370 bool nsec3narrowbool
= false;
371 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
372 if (nsec3narrow
== "1")
373 nsec3narrowbool
= true;
374 doc
["nsec3narrow"] = nsec3narrowbool
;
377 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
378 doc
["api_rectify"] = (api_rectify
== "1");
381 vector
<string
> tsig_master
, tsig_slave
;
382 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
383 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
385 Json::array tsig_master_keys
;
386 for (const auto& keyname
: tsig_master
) {
387 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
389 doc
["master_tsig_key_ids"] = tsig_master_keys
;
391 Json::array tsig_slave_keys
;
392 for (const auto& keyname
: tsig_slave
) {
393 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
395 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
398 vector
<DNSResourceRecord
> records
;
399 vector
<Comment
> comments
;
401 // load all records + sort
403 DNSResourceRecord rr
;
404 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
405 while(di
.backend
->get(rr
)) {
406 if (!rr
.qtype
.getCode())
407 continue; // skip empty non-terminals
408 records
.push_back(rr
);
410 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
411 /* if you ever want to update this comparison function,
412 please be aware that you will also need to update the conditions in the code merging
413 the records and comments below */
414 if (a
.qname
== b
.qname
) {
415 return b
.qtype
< a
.qtype
;
417 return b
.qname
< a
.qname
;
421 // load all comments + sort
424 di
.backend
->listComments(di
.id
);
425 while(di
.backend
->getComment(comment
)) {
426 comments
.push_back(comment
);
428 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
429 /* if you ever want to update this comparison function,
430 please be aware that you will also need to update the conditions in the code merging
431 the records and comments below */
432 if (a
.qname
== b
.qname
) {
433 return b
.qtype
< a
.qtype
;
435 return b
.qname
< a
.qname
;
441 Json::array rrset_records
;
442 Json::array rrset_comments
;
443 DNSName current_qname
;
446 auto rit
= records
.begin();
447 auto cit
= comments
.begin();
449 while (rit
!= records
.end() || cit
!= comments
.end()) {
450 // 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
451 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
452 current_qname
= rit
->qname
;
453 current_qtype
= rit
->qtype
;
456 current_qname
= cit
->qname
;
457 current_qtype
= cit
->qtype
;
461 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
462 ttl
= min(ttl
, rit
->ttl
);
463 rrset_records
.push_back(Json::object
{
464 { "disabled", rit
->disabled
},
465 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
469 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
470 rrset_comments
.push_back(Json::object
{
471 { "modified_at", (double)cit
->modified_at
},
472 { "account", cit
->account
},
473 { "content", cit
->content
}
478 rrset
["name"] = current_qname
.toString();
479 rrset
["type"] = current_qtype
.getName();
480 rrset
["records"] = rrset_records
;
481 rrset
["comments"] = rrset_comments
;
482 rrset
["ttl"] = (double)ttl
;
483 rrsets
.push_back(rrset
);
485 rrset_records
.clear();
486 rrset_comments
.clear();
489 doc
["rrsets"] = rrsets
;
495 void productServerStatisticsFetch(map
<string
,string
>& out
)
497 vector
<string
> items
= S
.getEntries();
498 for(const string
& item
: items
) {
499 out
[item
] = std::to_string(S
.read(item
));
503 out
["uptime"] = std::to_string(time(0) - s_starttime
);
506 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
509 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
517 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
518 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
519 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
523 static void gatherRecords(const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
525 DNSResourceRecord rr
;
531 validateGatheredRRType(rr
);
532 const auto& items
= container
["records"].array_items();
533 for(const auto& record
: items
) {
534 string content
= stringFromJson(record
, "content");
535 rr
.disabled
= boolFromJson(record
, "disabled");
537 // validate that the client sent something we can actually parse, and require that data to be dotted.
539 if (rr
.qtype
.getCode() != QType::AAAA
) {
540 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
541 if (!pdns_iequals(tmp
, content
)) {
542 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
545 struct in6_addr tmpbuf
;
546 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
547 throw std::runtime_error("Invalid IPv6 address");
550 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
552 catch(std::exception
& e
)
554 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
557 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
558 boolFromJson(record
, "set-ptr", false) == true) {
559 DNSResourceRecord ptr
;
562 // verify that there's a zone for the PTR
564 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
565 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
567 ptr
.domain_id
= sd
.domain_id
;
568 new_ptrs
.push_back(ptr
);
571 new_records
.push_back(rr
);
575 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
580 time_t now
= time(0);
581 for (auto comment
: container
["comments"].array_items()) {
582 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
583 c
.content
= stringFromJson(comment
, "content");
584 c
.account
= stringFromJson(comment
, "account");
585 new_comments
.push_back(c
);
589 static void checkDefaultDNSSECAlgos() {
590 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
591 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
592 int k_size
= arg().asNum("default-ksk-size");
593 int z_size
= arg().asNum("default-zsk-size");
595 // Sanity check DNSSEC parameters
596 if (::arg()["default-zsk-algorithm"] != "") {
598 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
599 else if (k_algo
<= 10 && k_size
== 0)
600 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
603 if (::arg()["default-zsk-algorithm"] != "") {
605 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
606 else if (z_algo
<= 10 && z_size
== 0)
607 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
611 static void throwUnableToSecure(const DNSName
& zonename
) {
612 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
613 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
616 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
) {
617 vector
<string
> zonemaster
;
618 bool shouldRectify
= false;
619 for(auto value
: document
["masters"].array_items()) {
620 string master
= value
.string_value();
622 throw ApiException("Master can not be an empty string");
624 ComboAddress
m(master
);
625 } catch (const PDNSException
&e
) {
626 throw ApiException("Master (" + master
+ ") is not an IP address: " + e
.reason
);
628 zonemaster
.push_back(master
);
631 if (zonemaster
.size()) {
632 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
634 if (document
["kind"].is_string()) {
635 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
637 if (document
["soa_edit_api"].is_string()) {
638 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
640 if (document
["soa_edit"].is_string()) {
641 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
644 bool api_rectify
= boolFromJson(document
, "api_rectify");
645 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
647 catch (const JsonException
&) {}
649 if (document
["account"].is_string()) {
650 di
.backend
->setAccount(zonename
, document
["account"].string_value());
654 bool dnssecInJSON
= false;
655 bool dnssecDocVal
= false;
658 dnssecDocVal
= boolFromJson(document
, "dnssec");
661 catch (const JsonException
&) {}
663 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
668 checkDefaultDNSSECAlgos();
670 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
671 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
672 int k_size
= arg().asNum("default-ksk-size");
673 int z_size
= arg().asNum("default-zsk-size");
677 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
678 throwUnableToSecure(zonename
);
684 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
685 throwUnableToSecure(zonename
);
689 // Used later for NSEC3PARAM
690 isDNSSECZone
= dk
.isSecuredZone(zonename
);
693 throwUnableToSecure(zonename
);
695 shouldRectify
= true;
698 // "dnssec": false in json
701 if (!dk
.unSecureZone(zonename
, error
, info
)) {
702 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
704 isDNSSECZone
= dk
.isSecuredZone(zonename
);
706 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
708 shouldRectify
= true;
713 if(document
["nsec3param"].string_value().length() > 0) {
714 shouldRectify
= true;
715 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
716 string error_msg
= "";
718 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
720 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
721 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
723 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
724 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
725 "' passed our basic sanity checks, but cannot be used with the current backend.");
729 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
732 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
733 if (api_rectify
.empty()) {
734 if (::arg().mustDo("default-api-rectify")) {
738 if (api_rectify
== "1") {
741 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true)) {
742 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
747 string soa_edit_api_kind
;
748 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
749 if (!soa_edit_api_kind
.empty()) {
751 if (!B
.getSOAUncached(zonename
, sd
))
754 string soa_edit_kind
;
755 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
757 DNSResourceRecord rr
;
758 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
759 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
760 throw ApiException("Hosting backend does not support editing records.");
766 if (!document
["master_tsig_key_ids"].is_null()) {
767 vector
<string
> metadata
;
770 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
771 auto keyname(apiZoneIdToName(value
.string_value()));
772 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
773 if (keyAlgo
.empty() || keyContent
.empty()) {
774 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
776 metadata
.push_back(keyname
.toString());
778 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
779 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
782 if (!document
["slave_tsig_key_ids"].is_null()) {
783 vector
<string
> metadata
;
786 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
787 auto keyname(apiZoneIdToName(value
.string_value()));
788 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
789 if (keyAlgo
.empty() || keyContent
.empty()) {
790 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
792 metadata
.push_back(keyname
.toString());
794 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
795 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
800 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
801 static vector
<string
> builtinOptions
{
804 "ALLOW-DNSUPDATE-FROM",
805 "TSIG-ALLOW-DNSUPDATE",
807 "SOA-EDIT-DNSUPDATE",
811 "GSS-ALLOW-AXFR-PRINCIPAL",
812 "GSS-ACCEPTOR-PRINCIPAL",
822 "TSIG-ALLOW-DNSUPDATE"
825 // the following options do not allow modifications via API
826 static vector
<string
> protectedOptions
{
836 if (kind
.find("X-") == 0)
841 for (const string
& s
: builtinOptions
) {
843 for (const string
& s2
: protectedOptions
) {
844 if (!readonly
&& s
== s2
)
855 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
856 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
860 if (!B
.getDomainInfo(zonename
, di
)) {
861 throw HttpNotFoundException();
864 if (req
->method
== "GET") {
865 map
<string
, vector
<string
> > md
;
866 Json::array document
;
868 if (!B
.getAllDomainMetadata(zonename
, md
))
869 throw HttpNotFoundException();
871 for (const auto& i
: md
) {
873 for (string j
: i
.second
)
874 entries
.push_back(j
);
877 { "type", "Metadata" },
879 { "metadata", entries
}
882 document
.push_back(key
);
885 resp
->setBody(document
);
886 } else if (req
->method
== "POST") {
887 auto document
= req
->json();
889 vector
<string
> entries
;
892 kind
= stringFromJson(document
, "kind");
893 } catch (const JsonException
&) {
894 throw ApiException("kind is not specified or not a string");
897 if (!isValidMetadataKind(kind
, false))
898 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
900 vector
<string
> vecMetadata
;
902 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
903 throw ApiException("Could not retrieve metadata entries for domain '" +
904 zonename
.toString() + "'");
906 auto& metadata
= document
["metadata"];
907 if (!metadata
.is_array())
908 throw ApiException("metadata is not specified or not an array");
910 for (const auto& i
: metadata
.array_items()) {
912 throw ApiException("metadata must be strings");
913 else if (std::find(vecMetadata
.cbegin(),
915 i
.string_value()) == vecMetadata
.cend()) {
916 vecMetadata
.push_back(i
.string_value());
920 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
921 throw ApiException("Could not update metadata entries for domain '" +
922 zonename
.toString() + "'");
924 Json::array respMetadata
;
925 for (const string
& s
: vecMetadata
)
926 respMetadata
.push_back(s
);
929 { "type", "Metadata" },
930 { "kind", document
["kind"] },
931 { "metadata", respMetadata
}
937 throw HttpMethodNotAllowedException();
940 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
941 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
945 if (!B
.getDomainInfo(zonename
, di
)) {
946 throw HttpNotFoundException();
949 string kind
= req
->parameters
["kind"];
951 if (req
->method
== "GET") {
952 vector
<string
> metadata
;
953 Json::object document
;
956 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
957 throw HttpNotFoundException();
958 else if (!isValidMetadataKind(kind
, true))
959 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
961 document
["type"] = "Metadata";
962 document
["kind"] = kind
;
964 for (const string
& i
: metadata
)
965 entries
.push_back(i
);
967 document
["metadata"] = entries
;
968 resp
->setBody(document
);
969 } else if (req
->method
== "PUT") {
970 auto document
= req
->json();
972 if (!isValidMetadataKind(kind
, false))
973 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
975 vector
<string
> vecMetadata
;
976 auto& metadata
= document
["metadata"];
977 if (!metadata
.is_array())
978 throw ApiException("metadata is not specified or not an array");
980 for (const auto& i
: metadata
.array_items()) {
982 throw ApiException("metadata must be strings");
983 vecMetadata
.push_back(i
.string_value());
986 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
987 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
990 { "type", "Metadata" },
992 { "metadata", metadata
}
996 } else if (req
->method
== "DELETE") {
997 if (!isValidMetadataKind(kind
, false))
998 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1000 vector
<string
> md
; // an empty vector will do it
1001 if (!B
.setDomainMetadata(zonename
, kind
, md
))
1002 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1004 throw HttpMethodNotAllowedException();
1007 // Throws 404 if the key with inquireKeyId does not exist
1008 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1009 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1011 for(const auto& value
: keyset
) {
1012 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1018 throw HttpNotFoundException();
1022 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1023 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1025 bool inquireSingleKey
= inquireKeyId
>= 0;
1028 for(const auto& value
: keyset
) {
1029 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1034 switch (value
.second
.keyType
) {
1035 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1036 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1037 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1041 { "type", "Cryptokey" },
1042 { "id", (int)value
.second
.id
},
1043 { "active", value
.second
.active
},
1044 { "keytype", keyType
},
1045 { "flags", (uint16_t)value
.first
.d_flags
},
1046 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1047 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1048 { "bits", value
.first
.getKey()->getBits() }
1051 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1053 for(const uint8_t keyid
: { DNSSECKeeper::SHA1
, DNSSECKeeper::SHA256
, DNSSECKeeper::GOST
, DNSSECKeeper::SHA384
})
1055 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1060 if (inquireSingleKey
) {
1061 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1068 if (inquireSingleKey
) {
1069 // we came here because we couldn't find the requested key.
1070 throw HttpNotFoundException();
1077 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1078 * It deletes a key from :zone_name specified by :cryptokey_id.
1080 * Case 1: the backend returns true on removal. This means the key is gone.
1081 * The server returns 204 No Content, no body.
1082 * Case 2: the backend returns false on removal. An error occurred.
1083 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1084 * Case 3: the key or zone does not exist.
1085 * The server returns 404 Not Found
1087 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1088 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1092 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1097 * This method adds a key to a zone by generate it or content parameter.
1100 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1101 * "keytype" : "ksk|zsk" <string>
1102 * "active" : "true|false" <value>
1103 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1104 * "bits" : number of bits <int>
1108 * Case 1: keytype isn't ksk|zsk
1109 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1110 * Case 2: 'bits' must be a positive integer value.
1111 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1112 * Case 3: The "algorithm" isn't supported
1113 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1114 * Case 4: Algorithm <= 10 and no bits were passed
1115 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1116 * Case 5: The wrong keysize was passed
1117 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1118 * Case 6: If the server cant guess the keysize
1119 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1120 * Case 7: The key-creation failed
1121 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1122 * Case 8: The key in content has the wrong format
1123 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1124 * Case 9: The wrong combination of fields is submitted
1125 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1126 * Case 10: No content and everything was fine
1127 * The server returns 201 Created and all public data about the new cryptokey
1128 * Case 11: With specified content
1129 * The server returns 201 Created and all public data about the added cryptokey
1132 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1133 auto document
= req
->json();
1134 string privatekey_fieldname
= "privatekey";
1135 auto privatekey
= document
["privatekey"];
1136 if (privatekey
.is_null()) {
1137 // Fallback to the old "content" behaviour
1138 privatekey
= document
["content"];
1139 privatekey_fieldname
= "content";
1141 bool active
= boolFromJson(document
, "active", false);
1144 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1146 } else if (stringFromJson(document
, "keytype") == "zsk") {
1149 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1152 int64_t insertedId
= -1;
1154 if (privatekey
.is_null()) {
1155 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1156 auto docbits
= document
["bits"];
1157 if (!docbits
.is_null()) {
1158 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1159 throw ApiException("'bits' must be a positive integer value");
1161 bits
= docbits
.int_value();
1164 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1165 auto providedAlgo
= document
["algorithm"];
1166 if (providedAlgo
.is_string()) {
1167 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1168 if (algorithm
== -1)
1169 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1170 } else if (providedAlgo
.is_number()) {
1171 algorithm
= providedAlgo
.int_value();
1172 } else if (!providedAlgo
.is_null()) {
1173 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1177 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
)) {
1178 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1180 } catch (std::runtime_error
& error
) {
1181 throw ApiException(error
.what());
1184 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1185 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1186 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1187 DNSKEYRecordContent dkrc
;
1188 DNSSECPrivateKey dpk
;
1190 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1191 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1192 // TODO remove in 4.2.0
1193 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1194 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1203 catch (std::runtime_error
& error
) {
1204 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1206 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
)) {
1207 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1209 } catch (std::runtime_error
& error
) {
1210 throw ApiException(error
.what());
1213 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1215 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1217 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1222 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1223 * It de/activates a key from :zone_name specified by :cryptokey_id.
1225 * Case 1: invalid JSON data
1226 * The server returns 400 Bad Request
1227 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1228 * The server returns 204 No Content
1229 * Case 3: the backend returns false on de/activation. An error occurred.
1230 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1232 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1233 //throws an exception if the Body is empty
1234 auto document
= req
->json();
1235 //throws an exception if the key does not exist or is not a bool
1236 bool active
= boolFromJson(document
, "active");
1238 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1239 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1243 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1244 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1254 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1255 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1256 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1258 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1259 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1262 DNSSECKeeper
dk(&B
);
1264 if (!B
.getDomainInfo(zonename
, di
)) {
1265 throw HttpNotFoundException();
1268 int inquireKeyId
= -1;
1269 if (req
->parameters
.count("key_id")) {
1270 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1271 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1274 if (req
->method
== "GET") {
1275 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1276 } else if (req
->method
== "DELETE") {
1277 if (inquireKeyId
== -1)
1278 throw HttpBadRequestException();
1279 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1280 } else if (req
->method
== "POST") {
1281 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1282 } else if (req
->method
== "PUT") {
1283 if (inquireKeyId
== -1)
1284 throw HttpBadRequestException();
1285 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1287 throw HttpMethodNotAllowedException(); //Returns method not allowed
1291 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1292 DNSResourceRecord rr
;
1293 vector
<string
> zonedata
;
1294 stringtok(zonedata
, zonestring
, "\r\n");
1296 ZoneParserTNG
zpt(zonedata
, zonename
);
1300 string comment
= "Imported via the API";
1303 while(zpt
.get(rr
, &comment
)) {
1304 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1306 if(rr
.qtype
.getCode() == QType::SOA
)
1308 validateGatheredRRType(rr
);
1310 new_records
.push_back(rr
);
1313 catch(std::exception
& ae
) {
1314 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1318 /** Throws ApiException if records which violate RRset contraints are present.
1319 * NOTE: sorts records in-place.
1321 * Constraints being checked:
1322 * *) no exact duplicates
1323 * *) no duplicates for QTypes that can only be present once per RRset
1324 * *) hostnames are hostnames
1326 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1327 sort(records
.begin(), records
.end(),
1328 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1329 /* we need _strict_ weak ordering */
1330 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1334 DNSResourceRecord previous
;
1335 for(const auto& rec
: records
) {
1336 if (previous
.qname
== rec
.qname
) {
1337 if (previous
.qtype
== rec
.qtype
) {
1338 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1339 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1341 if (previous
.content
== rec
.content
) {
1342 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1344 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1345 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1349 // Check if the DNSNames that should be hostnames, are hostnames
1351 checkHostnameCorrectness(rec
);
1352 } catch (const std::exception
& e
) {
1353 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1360 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1362 string contentFromDB
;
1363 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1364 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1365 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1369 if (!getTSIGHashEnum(algo
, the
)) {
1370 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1374 if (B64Decode(content
, b64out
) == -1) {
1375 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1379 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1380 Json::object tsigkey
= {
1381 { "name", keyname
.toStringNoDot() },
1382 { "id", apiZoneNameToId(keyname
) },
1383 { "algorithm", algo
.toStringNoDot() },
1385 { "type", "TSIGKey" }
1390 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1391 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1394 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1396 if (req
->method
== "GET") {
1397 vector
<struct TSIGKey
> keys
;
1399 if (!B
.getTSIGKeys(keys
)) {
1400 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1405 for(const auto &key
: keys
) {
1406 doc
.push_back(makeJSONTSIGKey(key
, false));
1409 } else if (req
->method
== "POST") {
1410 auto document
= req
->json();
1411 DNSName
keyname(stringFromJson(document
, "name"));
1412 DNSName
algo(stringFromJson(document
, "algorithm"));
1413 string content
= document
["key"].string_value();
1415 if (content
.empty()) {
1417 content
= makeTSIGKey(algo
);
1418 } catch (const PDNSException
& e
) {
1419 throw HttpBadRequestException(e
.reason
);
1423 // Will throw an ApiException or HttpConflictException on error
1424 checkTSIGKey(B
, keyname
, algo
, content
);
1426 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1427 throw HttpInternalServerErrorException("Unable to add TSIG key");
1431 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1433 throw HttpMethodNotAllowedException();
1437 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1439 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1443 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1444 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1449 tsk
.algorithm
= algo
;
1452 if (req
->method
== "GET") {
1453 resp
->setBody(makeJSONTSIGKey(tsk
));
1454 } else if (req
->method
== "PUT") {
1455 json11::Json document
;
1456 if (!req
->body
.empty()) {
1457 document
= req
->json();
1459 if (document
["name"].is_string()) {
1460 tsk
.name
= DNSName(document
["name"].string_value());
1462 if (document
["algorithm"].is_string()) {
1463 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1466 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1467 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1470 if (document
["key"].is_string()) {
1471 string new_content
= document
["key"].string_value();
1473 if (B64Decode(new_content
, decoded
) == -1) {
1474 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1476 tsk
.key
= new_content
;
1478 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1479 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1481 if (tsk
.name
!= keyname
) {
1482 // Remove the old key
1483 if (!B
.deleteTSIGKey(keyname
)) {
1484 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1487 resp
->setBody(makeJSONTSIGKey(tsk
));
1488 } else if (req
->method
== "DELETE") {
1489 if (!B
.deleteTSIGKey(keyname
)) {
1490 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1496 throw HttpMethodNotAllowedException();
1500 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1502 DNSSECKeeper
dk(&B
);
1503 if (req
->method
== "POST") {
1505 auto document
= req
->json();
1506 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1507 apiCheckNameAllowedCharacters(zonename
.toString());
1508 zonename
.makeUsLowerCase();
1510 bool exists
= B
.getDomainInfo(zonename
, di
);
1512 throw HttpConflictException();
1514 // validate 'kind' is set
1515 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1517 string zonestring
= document
["zone"].string_value();
1518 auto rrsets
= document
["rrsets"];
1519 if (rrsets
.is_array() && zonestring
!= "")
1520 throw ApiException("You cannot give rrsets AND zone data as text");
1522 auto nameservers
= document
["nameservers"];
1523 if (!nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1524 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
1526 string soa_edit_api_kind
;
1527 if (document
["soa_edit_api"].is_string()) {
1528 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1531 soa_edit_api_kind
= "DEFAULT";
1533 string soa_edit_kind
= document
["soa_edit"].string_value();
1535 // if records/comments are given, load and check them
1536 bool have_soa
= false;
1537 bool have_zone_ns
= false;
1538 vector
<DNSResourceRecord
> new_records
;
1539 vector
<Comment
> new_comments
;
1540 vector
<DNSResourceRecord
> new_ptrs
;
1542 if (rrsets
.is_array()) {
1543 for (const auto& rrset
: rrsets
.array_items()) {
1544 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1545 apiCheckQNameAllowedCharacters(qname
.toString());
1547 qtype
= stringFromJson(rrset
, "type");
1548 if (qtype
.getCode() == 0) {
1549 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1551 if (rrset
["records"].is_array()) {
1552 int ttl
= intFromJson(rrset
, "ttl");
1553 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1555 if (rrset
["comments"].is_array()) {
1556 gatherComments(rrset
, qname
, qtype
, new_comments
);
1559 } else if (zonestring
!= "") {
1560 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1563 for(auto& rr
: new_records
) {
1564 rr
.qname
.makeUsLowerCase();
1565 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1566 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1567 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1569 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1571 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1573 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1574 have_zone_ns
= true;
1578 // synthesize RRs as needed
1579 DNSResourceRecord autorr
;
1580 autorr
.qname
= zonename
;
1582 autorr
.ttl
= ::arg().asNum("default-ttl");
1584 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1585 // synthesize a SOA record so the zone "really" exists
1586 string soa
= (boost::format("%s %s %ul")
1587 % ::arg()["default-soa-name"]
1588 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1589 % document
["serial"].int_value()
1592 fillSOAData(soa
, sd
); // fills out default values for us
1593 autorr
.qtype
= QType::SOA
;
1594 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1595 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1596 new_records
.push_back(autorr
);
1599 // create NS records if nameservers are given
1600 for (auto value
: nameservers
.array_items()) {
1601 string nameserver
= value
.string_value();
1602 if (nameserver
.empty())
1603 throw ApiException("Nameservers must be non-empty strings");
1604 if (!isCanonical(nameserver
))
1605 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1607 // ensure the name parses
1608 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1610 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1612 autorr
.qtype
= QType::NS
;
1613 new_records
.push_back(autorr
);
1615 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1619 checkNewRecords(new_records
);
1621 if (boolFromJson(document
, "dnssec", false)) {
1622 checkDefaultDNSSECAlgos();
1624 if(document
["nsec3param"].string_value().length() > 0) {
1625 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1626 string error_msg
= "";
1627 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1628 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1633 // no going back after this
1634 if(!B
.createDomain(zonename
))
1635 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1637 if(!B
.getDomainInfo(zonename
, di
))
1638 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1640 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1641 if (!soa_edit_api_kind
.empty()) {
1642 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1645 di
.backend
->startTransaction(zonename
, di
.id
);
1647 for(auto rr
: new_records
) {
1648 rr
.domain_id
= di
.id
;
1649 di
.backend
->feedRecord(rr
, DNSName());
1651 for(Comment
& c
: new_comments
) {
1652 c
.domain_id
= di
.id
;
1653 di
.backend
->feedComment(c
);
1656 updateDomainSettingsFromDocument(B
, di
, zonename
, document
);
1658 di
.backend
->commitTransaction();
1660 storeChangedPTRs(B
, new_ptrs
);
1662 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1667 if(req
->method
!= "GET")
1668 throw HttpMethodNotAllowedException();
1670 vector
<DomainInfo
> domains
;
1672 if (req
->getvars
.count("zone")) {
1673 string zone
= req
->getvars
["zone"];
1674 apiCheckNameAllowedCharacters(zone
);
1675 DNSName zonename
= apiNameToDNSName(zone
);
1676 zonename
.makeUsLowerCase();
1678 if (B
.getDomainInfo(zonename
, di
)) {
1679 domains
.push_back(di
);
1682 B
.getAllDomains(&domains
, true); // incl. disabled
1686 for(const DomainInfo
& di
: domains
) {
1687 doc
.push_back(getZoneInfo(di
, &dk
));
1692 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1693 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1698 if (!B
.getDomainInfo(zonename
, di
)) {
1699 throw HttpNotFoundException();
1701 } catch(const PDNSException
&e
) {
1702 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1705 if(req
->method
== "PUT") {
1706 // update domain settings
1708 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json());
1711 resp
->status
= 204; // No Content, but indicate success
1714 else if(req
->method
== "DELETE") {
1716 if(!di
.backend
->deleteDomain(zonename
))
1717 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1719 // empty body on success
1721 resp
->status
= 204; // No Content: declare that the zone is gone now
1723 } else if (req
->method
== "PATCH") {
1724 patchZone(req
, resp
);
1726 } else if (req
->method
== "GET") {
1727 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1730 throw HttpMethodNotAllowedException();
1733 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1734 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1736 if(req
->method
!= "GET")
1737 throw HttpMethodNotAllowedException();
1743 if (!B
.getDomainInfo(zonename
, di
)) {
1744 throw HttpNotFoundException();
1747 DNSResourceRecord rr
;
1749 di
.backend
->list(zonename
, di
.id
);
1750 while(di
.backend
->get(rr
)) {
1751 if (!rr
.qtype
.getCode())
1752 continue; // skip empty non-terminals
1755 rr
.qname
.toString() << "\t" <<
1758 rr
.qtype
.getName() << "\t" <<
1759 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1763 if (req
->accept_json
) {
1764 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1766 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1767 resp
->body
= ss
.str();
1771 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1772 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1774 if(req
->method
!= "PUT")
1775 throw HttpMethodNotAllowedException();
1779 if (!B
.getDomainInfo(zonename
, di
)) {
1780 throw HttpNotFoundException();
1783 if(di
.masters
.empty())
1784 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1786 random_shuffle(di
.masters
.begin(), di
.masters
.end());
1787 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1788 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1791 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1792 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1794 if(req
->method
!= "PUT")
1795 throw HttpMethodNotAllowedException();
1799 if (!B
.getDomainInfo(zonename
, di
)) {
1800 throw HttpNotFoundException();
1803 if(!Communicator
.notifyDomain(zonename
))
1804 throw ApiException("Failed to add to the queue - see server log");
1806 resp
->setSuccessResult("Notification queued");
1809 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1810 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1812 if(req
->method
!= "PUT")
1813 throw HttpMethodNotAllowedException();
1817 if (!B
.getDomainInfo(zonename
, di
)) {
1818 throw HttpNotFoundException();
1821 DNSSECKeeper
dk(&B
);
1823 if (!dk
.isSecuredZone(zonename
))
1824 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1826 if (di
.kind
== DomainInfo::Slave
)
1827 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1829 string error_msg
= "";
1831 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1832 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1834 resp
->setSuccessResult("Rectified");
1837 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1838 if (rr
.qtype
.getCode() == QType::A
) {
1840 if (!IpToU32(rr
.content
, &ip
)) {
1841 throw ApiException("PTR: Invalid IP address given");
1843 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1844 % ((ip
>> 24) & 0xff)
1845 % ((ip
>> 16) & 0xff)
1846 % ((ip
>> 8) & 0xff)
1849 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1850 ComboAddress
ca(rr
.content
);
1853 for (int octet
= 0; octet
< 16; ++octet
) {
1854 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1855 // this should be impossible: no byte should give more than two digits in hex format
1856 throw PDNSException("Formatting IPv6 address failed");
1858 ss
<< buf
[0] << '.' << buf
[1] << '.';
1860 string tmp
= ss
.str();
1861 tmp
.resize(tmp
.size()-1); // remove last dot
1862 // reverse and append arpa domain
1863 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1865 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1870 ptr
->disabled
= rr
.disabled
;
1871 ptr
->content
= rr
.qname
.toStringRootDot();
1874 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1875 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1877 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1878 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1880 string soa_edit_api_kind
;
1881 string soa_edit_kind
;
1882 bool soa_changed
= false;
1883 DNSResourceRecord soarr
;
1884 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1885 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1886 if (!soa_edit_api_kind
.empty()) {
1887 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1890 sd
.db
->startTransaction(sd
.qname
);
1891 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1892 sd
.db
->abortTransaction();
1893 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1897 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1900 sd
.db
->commitTransaction();
1901 purgeAuthCachesExact(rr
.qname
);
1905 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
) {
1908 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1909 if (!B
.getDomainInfo(zonename
, di
)) {
1910 throw HttpNotFoundException();
1913 vector
<DNSResourceRecord
> new_records
;
1914 vector
<Comment
> new_comments
;
1915 vector
<DNSResourceRecord
> new_ptrs
;
1917 Json document
= req
->json();
1919 auto rrsets
= document
["rrsets"];
1920 if (!rrsets
.is_array())
1921 throw ApiException("No rrsets given in update request");
1923 di
.backend
->startTransaction(zonename
);
1926 string soa_edit_api_kind
;
1927 string soa_edit_kind
;
1928 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1929 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1930 bool soa_edit_done
= false;
1932 set
<pair
<DNSName
, QType
>> seen
;
1934 for (const auto& rrset
: rrsets
.array_items()) {
1935 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1936 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1937 apiCheckQNameAllowedCharacters(qname
.toString());
1939 qtype
= stringFromJson(rrset
, "type");
1940 if (qtype
.getCode() == 0) {
1941 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1944 if(seen
.count({qname
, qtype
}))
1946 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1948 seen
.insert({qname
, qtype
});
1950 if (changetype
== "DELETE") {
1951 // delete all matching qname/qtype RRs (and, implicitly comments).
1952 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1953 throw ApiException("Hosting backend does not support editing records.");
1956 else if (changetype
== "REPLACE") {
1957 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1958 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
1959 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
1961 bool replace_records
= rrset
["records"].is_array();
1962 bool replace_comments
= rrset
["comments"].is_array();
1964 if (!replace_records
&& !replace_comments
) {
1965 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
1968 new_records
.clear();
1969 new_comments
.clear();
1971 if (replace_records
) {
1972 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1973 int ttl
= intFromJson(rrset
, "ttl");
1974 // new_ptrs is merged.
1975 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1977 for(DNSResourceRecord
& rr
: new_records
) {
1978 rr
.domain_id
= di
.id
;
1979 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1980 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1983 checkNewRecords(new_records
);
1986 if (replace_comments
) {
1987 gatherComments(rrset
, qname
, qtype
, new_comments
);
1989 for(Comment
& c
: new_comments
) {
1990 c
.domain_id
= di
.id
;
1994 if (replace_records
) {
1995 bool ent_present
= false;
1996 di
.backend
->lookup(QType(QType::ANY
), qname
);
1997 DNSResourceRecord rr
;
1998 while (di
.backend
->get(rr
)) {
1999 if (qtype
.getCode() == 0) {
2002 if (qtype
.getCode() != rr
.qtype
.getCode()
2003 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2004 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2005 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2009 if (!new_records
.empty() && ent_present
) {
2011 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2012 throw ApiException("Hosting backend does not support editing records.");
2015 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2016 throw ApiException("Hosting backend does not support editing records.");
2019 if (replace_comments
) {
2020 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2021 throw ApiException("Hosting backend does not support editing comments.");
2026 throw ApiException("Changetype not understood");
2029 // edit SOA (if needed)
2030 if (!soa_edit_api_kind
.empty() && !soa_edit_done
) {
2032 if (!B
.getSOAUncached(zonename
, sd
))
2033 throw ApiException("No SOA found for domain '"+zonename
.toString()+"'");
2035 DNSResourceRecord rr
;
2036 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2037 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2038 throw ApiException("Hosting backend does not support editing records.");
2042 // return old and new serials in headers
2043 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2044 fillSOAData(rr
.content
, sd
);
2045 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2049 di
.backend
->abortTransaction();
2053 DNSSECKeeper
dk(&B
);
2055 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
2056 if (dk
.isSecuredZone(zonename
) && !dk
.isPresigned(zonename
) && api_rectify
== "1") {
2057 string error_msg
= "";
2059 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false))
2060 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2063 di
.backend
->commitTransaction();
2065 purgeAuthCachesExact(zonename
);
2068 storeChangedPTRs(B
, new_ptrs
);
2071 resp
->status
= 204; // No Content, but indicate success
2075 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2076 if(req
->method
!= "GET")
2077 throw HttpMethodNotAllowedException();
2079 string q
= req
->getvars
["q"];
2080 string sMax
= req
->getvars
["max"];
2081 string sObjectType
= req
->getvars
["object_type"];
2086 // the following types of data can be searched for using the api
2087 enum class ObjectType
2096 throw ApiException("Query q can't be blank");
2098 maxEnts
= std::stoi(sMax
);
2100 throw ApiException("Maximum entries must be larger than 0");
2102 if (sObjectType
.empty())
2103 objectType
= ObjectType::ALL
;
2104 else if (sObjectType
== "all")
2105 objectType
= ObjectType::ALL
;
2106 else if (sObjectType
== "zone")
2107 objectType
= ObjectType::ZONE
;
2108 else if (sObjectType
== "record")
2109 objectType
= ObjectType::RECORD
;
2110 else if (sObjectType
== "comment")
2111 objectType
= ObjectType::COMMENT
;
2113 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2115 SimpleMatch
sm(q
,true);
2117 vector
<DomainInfo
> domains
;
2118 vector
<DNSResourceRecord
> result_rr
;
2119 vector
<Comment
> result_c
;
2120 map
<int,DomainInfo
> zoneIdZone
;
2121 map
<int,DomainInfo
>::iterator val
;
2124 B
.getAllDomains(&domains
, true);
2126 for(const DomainInfo di
: domains
)
2128 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2129 doc
.push_back(Json::object
{
2130 { "object_type", "zone" },
2131 { "zone_id", apiZoneNameToId(di
.zone
) },
2132 { "name", di
.zone
.toString() }
2136 zoneIdZone
[di
.id
] = di
; // populate cache
2139 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2141 for(const DNSResourceRecord
& rr
: result_rr
)
2143 if (!rr
.qtype
.getCode())
2144 continue; // skip empty non-terminals
2146 auto object
= Json::object
{
2147 { "object_type", "record" },
2148 { "name", rr
.qname
.toString() },
2149 { "type", rr
.qtype
.getName() },
2150 { "ttl", (double)rr
.ttl
},
2151 { "disabled", rr
.disabled
},
2152 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2154 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2155 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2156 object
["zone"] = val
->second
.zone
.toString();
2158 doc
.push_back(object
);
2162 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2164 for(const Comment
&c
: result_c
)
2166 auto object
= Json::object
{
2167 { "object_type", "comment" },
2168 { "name", c
.qname
.toString() },
2169 { "content", c
.content
}
2171 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2172 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2173 object
["zone"] = val
->second
.zone
.toString();
2175 doc
.push_back(object
);
2182 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2183 if(req
->method
!= "PUT")
2184 throw HttpMethodNotAllowedException();
2186 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2188 uint64_t count
= purgeAuthCachesExact(canon
);
2189 resp
->setBody(Json::object
{
2190 { "count", (int) count
},
2191 { "result", "Flushed cache." }
2195 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2197 resp
->headers
["Cache-Control"] = "max-age=86400";
2198 resp
->headers
["Content-Type"] = "text/css";
2201 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2202 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2203 ret
<<"a { color: #0959c2; }"<<endl
;
2204 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2205 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2206 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2207 ret
<<".row:after { clear: both; }"<<endl
;
2208 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2209 ret
<<".all { width: 100%; }"<<endl
;
2210 ret
<<".headl { width: 60%; }"<<endl
;
2211 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2212 ret
<<"background-image: url(data:image/png;base64,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);";
2213 ret
<<" width: 154px; height: 20px; }"<<endl
;
2214 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2215 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2216 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2217 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2218 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2219 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2220 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2221 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2222 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2223 ret
<<".resetring {float: right; }"<<endl
;
2224 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
;
2225 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2226 ret
<<".resizering {float: right;}"<<endl
;
2227 resp
->body
= ret
.str();
2231 void AuthWebServer::webThread()
2234 setThreadName("pdns/webserver");
2235 if(::arg().mustDo("api")) {
2236 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2237 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2238 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2239 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2240 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2241 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2242 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2243 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2244 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2245 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2246 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2247 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2248 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2249 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2250 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2251 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2252 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2253 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2254 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2256 if (::arg().mustDo("webserver")) {
2257 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2258 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2263 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;