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