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 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) {
561 DNSResourceRecord ptr
;
564 // verify that there's a zone for the PTR
566 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
567 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
569 ptr
.domain_id
= sd
.domain_id
;
570 new_ptrs
.push_back(ptr
);
573 new_records
.push_back(rr
);
577 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
582 time_t now
= time(0);
583 for (auto comment
: container
["comments"].array_items()) {
584 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
585 c
.content
= stringFromJson(comment
, "content");
586 c
.account
= stringFromJson(comment
, "account");
587 new_comments
.push_back(c
);
591 static void checkDefaultDNSSECAlgos() {
592 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
593 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
594 int k_size
= arg().asNum("default-ksk-size");
595 int z_size
= arg().asNum("default-zsk-size");
597 // Sanity check DNSSEC parameters
598 if (::arg()["default-zsk-algorithm"] != "") {
600 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
601 else if (k_algo
<= 10 && k_size
== 0)
602 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
605 if (::arg()["default-zsk-algorithm"] != "") {
607 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
608 else if (z_algo
<= 10 && z_size
== 0)
609 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
613 static void throwUnableToSecure(const DNSName
& zonename
) {
614 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
615 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
618 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
) {
619 vector
<string
> zonemaster
;
620 bool shouldRectify
= false;
621 for(auto value
: document
["masters"].array_items()) {
622 string master
= value
.string_value();
624 throw ApiException("Master can not be an empty string");
625 zonemaster
.push_back(master
);
628 if (zonemaster
.size()) {
629 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
631 if (document
["kind"].is_string()) {
632 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
634 if (document
["soa_edit_api"].is_string()) {
635 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
637 if (document
["soa_edit"].is_string()) {
638 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
641 bool api_rectify
= boolFromJson(document
, "api_rectify");
642 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
644 catch (const JsonException
&) {}
646 if (document
["account"].is_string()) {
647 di
.backend
->setAccount(zonename
, document
["account"].string_value());
651 bool dnssecInJSON
= false;
652 bool dnssecDocVal
= false;
655 dnssecDocVal
= boolFromJson(document
, "dnssec");
658 catch (const JsonException
&) {}
660 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
665 checkDefaultDNSSECAlgos();
667 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
668 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
669 int k_size
= arg().asNum("default-ksk-size");
670 int z_size
= arg().asNum("default-zsk-size");
674 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
675 throwUnableToSecure(zonename
);
681 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
682 throwUnableToSecure(zonename
);
686 // Used later for NSEC3PARAM
687 isDNSSECZone
= dk
.isSecuredZone(zonename
);
690 throwUnableToSecure(zonename
);
692 shouldRectify
= true;
695 // "dnssec": false in json
698 if (!dk
.unSecureZone(zonename
, error
, info
)) {
699 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
701 isDNSSECZone
= dk
.isSecuredZone(zonename
);
703 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
705 shouldRectify
= true;
710 if(document
["nsec3param"].string_value().length() > 0) {
711 shouldRectify
= true;
712 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
713 string error_msg
= "";
715 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
717 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
718 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
720 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
721 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
722 "' passed our basic sanity checks, but cannot be used with the current backend.");
726 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
729 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
730 if (api_rectify
.empty()) {
731 if (::arg().mustDo("default-api-rectify")) {
735 if (api_rectify
== "1") {
738 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true)) {
739 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
744 string soa_edit_api_kind
;
745 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
746 if (!soa_edit_api_kind
.empty()) {
748 if (!B
.getSOAUncached(zonename
, sd
))
751 string soa_edit_kind
;
752 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
754 DNSResourceRecord rr
;
755 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
756 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
757 throw ApiException("Hosting backend does not support editing records.");
763 if (!document
["master_tsig_key_ids"].is_null()) {
764 vector
<string
> metadata
;
767 for(auto value
: document
["master_tsig_key_ids"].array_items()) {
768 auto keyname(apiZoneIdToName(value
.string_value()));
769 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
770 if (keyAlgo
.empty() || keyContent
.empty()) {
771 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
773 metadata
.push_back(keyname
.toString());
775 if (!di
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
776 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
779 if (!document
["slave_tsig_key_ids"].is_null()) {
780 vector
<string
> metadata
;
783 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
784 auto keyname(apiZoneIdToName(value
.string_value()));
785 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
786 if (keyAlgo
.empty() || keyContent
.empty()) {
787 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
789 metadata
.push_back(keyname
.toString());
791 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
792 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
797 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
798 static vector
<string
> builtinOptions
{
801 "ALLOW-DNSUPDATE-FROM",
802 "TSIG-ALLOW-DNSUPDATE",
804 "SOA-EDIT-DNSUPDATE",
808 "GSS-ALLOW-AXFR-PRINCIPAL",
809 "GSS-ACCEPTOR-PRINCIPAL",
819 "TSIG-ALLOW-DNSUPDATE"
822 // the following options do not allow modifications via API
823 static vector
<string
> protectedOptions
{
833 if (kind
.find("X-") == 0)
838 for (const string
& s
: builtinOptions
) {
840 for (const string
& s2
: protectedOptions
) {
841 if (!readonly
&& s
== s2
)
852 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
853 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
857 if (!B
.getDomainInfo(zonename
, di
)) {
858 throw HttpNotFoundException();
861 if (req
->method
== "GET") {
862 map
<string
, vector
<string
> > md
;
863 Json::array document
;
865 if (!B
.getAllDomainMetadata(zonename
, md
))
866 throw HttpNotFoundException();
868 for (const auto& i
: md
) {
870 for (string j
: i
.second
)
871 entries
.push_back(j
);
874 { "type", "Metadata" },
876 { "metadata", entries
}
879 document
.push_back(key
);
882 resp
->setBody(document
);
883 } else if (req
->method
== "POST") {
884 auto document
= req
->json();
886 vector
<string
> entries
;
889 kind
= stringFromJson(document
, "kind");
890 } catch (const JsonException
&) {
891 throw ApiException("kind is not specified or not a string");
894 if (!isValidMetadataKind(kind
, false))
895 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
897 vector
<string
> vecMetadata
;
899 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
900 throw ApiException("Could not retrieve metadata entries for domain '" +
901 zonename
.toString() + "'");
903 auto& metadata
= document
["metadata"];
904 if (!metadata
.is_array())
905 throw ApiException("metadata is not specified or not an array");
907 for (const auto& i
: metadata
.array_items()) {
909 throw ApiException("metadata must be strings");
910 else if (std::find(vecMetadata
.cbegin(),
912 i
.string_value()) == vecMetadata
.cend()) {
913 vecMetadata
.push_back(i
.string_value());
917 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
918 throw ApiException("Could not update metadata entries for domain '" +
919 zonename
.toString() + "'");
921 Json::array respMetadata
;
922 for (const string
& s
: vecMetadata
)
923 respMetadata
.push_back(s
);
926 { "type", "Metadata" },
927 { "kind", document
["kind"] },
928 { "metadata", respMetadata
}
934 throw HttpMethodNotAllowedException();
937 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
938 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
942 if (!B
.getDomainInfo(zonename
, di
)) {
943 throw HttpNotFoundException();
946 string kind
= req
->parameters
["kind"];
948 if (req
->method
== "GET") {
949 vector
<string
> metadata
;
950 Json::object document
;
953 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
954 throw HttpNotFoundException();
955 else if (!isValidMetadataKind(kind
, true))
956 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
958 document
["type"] = "Metadata";
959 document
["kind"] = kind
;
961 for (const string
& i
: metadata
)
962 entries
.push_back(i
);
964 document
["metadata"] = entries
;
965 resp
->setBody(document
);
966 } else if (req
->method
== "PUT") {
967 auto document
= req
->json();
969 if (!isValidMetadataKind(kind
, false))
970 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
972 vector
<string
> vecMetadata
;
973 auto& metadata
= document
["metadata"];
974 if (!metadata
.is_array())
975 throw ApiException("metadata is not specified or not an array");
977 for (const auto& i
: metadata
.array_items()) {
979 throw ApiException("metadata must be strings");
980 vecMetadata
.push_back(i
.string_value());
983 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
984 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
987 { "type", "Metadata" },
989 { "metadata", metadata
}
993 } else if (req
->method
== "DELETE") {
994 if (!isValidMetadataKind(kind
, false))
995 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
997 vector
<string
> md
; // an empty vector will do it
998 if (!B
.setDomainMetadata(zonename
, kind
, md
))
999 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1001 throw HttpMethodNotAllowedException();
1004 // Throws 404 if the key with inquireKeyId does not exist
1005 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1006 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1008 for(const auto& value
: keyset
) {
1009 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1015 throw HttpNotFoundException();
1019 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1020 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1022 bool inquireSingleKey
= inquireKeyId
>= 0;
1025 for(const auto& value
: keyset
) {
1026 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1031 switch (value
.second
.keyType
) {
1032 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1033 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1034 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1038 { "type", "Cryptokey" },
1039 { "id", (int)value
.second
.id
},
1040 { "active", value
.second
.active
},
1041 { "keytype", keyType
},
1042 { "flags", (uint16_t)value
.first
.d_flags
},
1043 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1044 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1045 { "bits", value
.first
.getKey()->getBits() }
1048 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1050 for(const uint8_t keyid
: { DNSSECKeeper::SHA1
, DNSSECKeeper::SHA256
, DNSSECKeeper::GOST
, DNSSECKeeper::SHA384
})
1052 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1057 if (inquireSingleKey
) {
1058 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1065 if (inquireSingleKey
) {
1066 // we came here because we couldn't find the requested key.
1067 throw HttpNotFoundException();
1074 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1075 * It deletes a key from :zone_name specified by :cryptokey_id.
1077 * Case 1: the backend returns true on removal. This means the key is gone.
1078 * The server returns 204 No Content, no body.
1079 * Case 2: the backend returns false on removal. An error occurred.
1080 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1081 * Case 3: the key or zone does not exist.
1082 * The server returns 404 Not Found
1084 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1085 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1089 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1094 * This method adds a key to a zone by generate it or content parameter.
1097 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1098 * "keytype" : "ksk|zsk" <string>
1099 * "active" : "true|false" <value>
1100 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1101 * "bits" : number of bits <int>
1105 * Case 1: keytype isn't ksk|zsk
1106 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1107 * Case 2: 'bits' must be a positive integer value.
1108 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1109 * Case 3: The "algorithm" isn't supported
1110 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1111 * Case 4: Algorithm <= 10 and no bits were passed
1112 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1113 * Case 5: The wrong keysize was passed
1114 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1115 * Case 6: If the server cant guess the keysize
1116 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1117 * Case 7: The key-creation failed
1118 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1119 * Case 8: The key in content has the wrong format
1120 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1121 * Case 9: The wrong combination of fields is submitted
1122 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1123 * Case 10: No content and everything was fine
1124 * The server returns 201 Created and all public data about the new cryptokey
1125 * Case 11: With specified content
1126 * The server returns 201 Created and all public data about the added cryptokey
1129 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1130 auto document
= req
->json();
1131 string privatekey_fieldname
= "privatekey";
1132 auto privatekey
= document
["privatekey"];
1133 if (privatekey
.is_null()) {
1134 // Fallback to the old "content" behaviour
1135 privatekey
= document
["content"];
1136 privatekey_fieldname
= "content";
1138 bool active
= boolFromJson(document
, "active", false);
1141 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1143 } else if (stringFromJson(document
, "keytype") == "zsk") {
1146 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1149 int64_t insertedId
= -1;
1151 if (privatekey
.is_null()) {
1152 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1153 auto docbits
= document
["bits"];
1154 if (!docbits
.is_null()) {
1155 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1156 throw ApiException("'bits' must be a positive integer value");
1158 bits
= docbits
.int_value();
1161 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1162 auto providedAlgo
= document
["algorithm"];
1163 if (providedAlgo
.is_string()) {
1164 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1165 if (algorithm
== -1)
1166 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1167 } else if (providedAlgo
.is_number()) {
1168 algorithm
= providedAlgo
.int_value();
1169 } else if (!providedAlgo
.is_null()) {
1170 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1174 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
)) {
1175 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1177 } catch (std::runtime_error
& error
) {
1178 throw ApiException(error
.what());
1181 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1182 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1183 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1184 DNSKEYRecordContent dkrc
;
1185 DNSSECPrivateKey dpk
;
1187 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1188 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1189 // TODO remove in 4.2.0
1190 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1191 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1200 catch (std::runtime_error
& error
) {
1201 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1203 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
)) {
1204 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1206 } catch (std::runtime_error
& error
) {
1207 throw ApiException(error
.what());
1210 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1212 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1214 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1219 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1220 * It de/activates a key from :zone_name specified by :cryptokey_id.
1222 * Case 1: invalid JSON data
1223 * The server returns 400 Bad Request
1224 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1225 * The server returns 204 No Content
1226 * Case 3: the backend returns false on de/activation. An error occurred.
1227 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1229 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1230 //throws an exception if the Body is empty
1231 auto document
= req
->json();
1232 //throws an exception if the key does not exist or is not a bool
1233 bool active
= boolFromJson(document
, "active");
1235 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1236 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1240 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1241 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1251 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1252 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1253 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1255 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1256 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1259 DNSSECKeeper
dk(&B
);
1261 if (!B
.getDomainInfo(zonename
, di
)) {
1262 throw HttpNotFoundException();
1265 int inquireKeyId
= -1;
1266 if (req
->parameters
.count("key_id")) {
1267 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1268 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1271 if (req
->method
== "GET") {
1272 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1273 } else if (req
->method
== "DELETE") {
1274 if (inquireKeyId
== -1)
1275 throw HttpBadRequestException();
1276 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1277 } else if (req
->method
== "POST") {
1278 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1279 } else if (req
->method
== "PUT") {
1280 if (inquireKeyId
== -1)
1281 throw HttpBadRequestException();
1282 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1284 throw HttpMethodNotAllowedException(); //Returns method not allowed
1288 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1289 DNSResourceRecord rr
;
1290 vector
<string
> zonedata
;
1291 stringtok(zonedata
, zonestring
, "\r\n");
1293 ZoneParserTNG
zpt(zonedata
, zonename
);
1297 string comment
= "Imported via the API";
1300 while(zpt
.get(rr
, &comment
)) {
1301 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1303 if(rr
.qtype
.getCode() == QType::SOA
)
1305 validateGatheredRRType(rr
);
1307 new_records
.push_back(rr
);
1310 catch(std::exception
& ae
) {
1311 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1315 /** Throws ApiException if records which violate RRset contraints are present.
1316 * NOTE: sorts records in-place.
1318 * Constraints being checked:
1319 * *) no exact duplicates
1320 * *) no duplicates for QTypes that can only be present once per RRset
1321 * *) hostnames are hostnames
1323 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1324 sort(records
.begin(), records
.end(),
1325 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1326 /* we need _strict_ weak ordering */
1327 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1331 DNSResourceRecord previous
;
1332 for(const auto& rec
: records
) {
1333 if (previous
.qname
== rec
.qname
) {
1334 if (previous
.qtype
== rec
.qtype
) {
1335 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1336 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1338 if (previous
.content
== rec
.content
) {
1339 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1341 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1342 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1346 // Check if the DNSNames that should be hostnames, are hostnames
1348 checkHostnameCorrectness(rec
);
1349 } catch (const std::exception
& e
) {
1350 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1357 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1359 string contentFromDB
;
1360 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1361 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1362 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1366 if (!getTSIGHashEnum(algo
, the
)) {
1367 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1371 if (B64Decode(content
, b64out
) == -1) {
1372 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1376 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1377 Json::object tsigkey
= {
1378 { "name", keyname
.toStringNoDot() },
1379 { "id", apiZoneNameToId(keyname
) },
1380 { "algorithm", algo
.toStringNoDot() },
1382 { "type", "TSIGKey" }
1387 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1388 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1391 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1393 if (req
->method
== "GET") {
1394 vector
<struct TSIGKey
> keys
;
1396 if (!B
.getTSIGKeys(keys
)) {
1397 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1402 for(const auto &key
: keys
) {
1403 doc
.push_back(makeJSONTSIGKey(key
, false));
1406 } else if (req
->method
== "POST") {
1407 auto document
= req
->json();
1408 DNSName
keyname(stringFromJson(document
, "name"));
1409 DNSName
algo(stringFromJson(document
, "algorithm"));
1410 string content
= document
["key"].string_value();
1412 if (content
.empty()) {
1414 content
= makeTSIGKey(algo
);
1415 } catch (const PDNSException
& e
) {
1416 throw HttpBadRequestException(e
.reason
);
1420 // Will throw an ApiException or HttpConflictException on error
1421 checkTSIGKey(B
, keyname
, algo
, content
);
1423 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1424 throw HttpInternalServerErrorException("Unable to add TSIG key");
1428 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1430 throw HttpMethodNotAllowedException();
1434 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1436 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1440 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1441 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1446 tsk
.algorithm
= algo
;
1449 if (req
->method
== "GET") {
1450 resp
->setBody(makeJSONTSIGKey(tsk
));
1451 } else if (req
->method
== "PUT") {
1452 json11::Json document
;
1453 if (!req
->body
.empty()) {
1454 document
= req
->json();
1456 if (document
["name"].is_string()) {
1457 tsk
.name
= DNSName(document
["name"].string_value());
1459 if (document
["algorithm"].is_string()) {
1460 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1463 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1464 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1467 if (document
["key"].is_string()) {
1468 string new_content
= document
["key"].string_value();
1470 if (B64Decode(new_content
, decoded
) == -1) {
1471 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1473 tsk
.key
= new_content
;
1475 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1476 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1478 if (tsk
.name
!= keyname
) {
1479 // Remove the old key
1480 if (!B
.deleteTSIGKey(keyname
)) {
1481 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1484 resp
->setBody(makeJSONTSIGKey(tsk
));
1485 } else if (req
->method
== "DELETE") {
1486 if (!B
.deleteTSIGKey(keyname
)) {
1487 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1493 throw HttpMethodNotAllowedException();
1497 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1499 DNSSECKeeper
dk(&B
);
1500 if (req
->method
== "POST") {
1502 auto document
= req
->json();
1503 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1504 apiCheckNameAllowedCharacters(zonename
.toString());
1505 zonename
.makeUsLowerCase();
1507 bool exists
= B
.getDomainInfo(zonename
, di
);
1509 throw HttpConflictException();
1511 // validate 'kind' is set
1512 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1514 string zonestring
= document
["zone"].string_value();
1515 auto rrsets
= document
["rrsets"];
1516 if (rrsets
.is_array() && zonestring
!= "")
1517 throw ApiException("You cannot give rrsets AND zone data as text");
1519 auto nameservers
= document
["nameservers"];
1520 if (!nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1521 throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
1523 string soa_edit_api_kind
;
1524 if (document
["soa_edit_api"].is_string()) {
1525 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1528 soa_edit_api_kind
= "DEFAULT";
1530 string soa_edit_kind
= document
["soa_edit"].string_value();
1532 // if records/comments are given, load and check them
1533 bool have_soa
= false;
1534 bool have_zone_ns
= false;
1535 vector
<DNSResourceRecord
> new_records
;
1536 vector
<Comment
> new_comments
;
1537 vector
<DNSResourceRecord
> new_ptrs
;
1539 if (rrsets
.is_array()) {
1540 for (const auto& rrset
: rrsets
.array_items()) {
1541 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1542 apiCheckQNameAllowedCharacters(qname
.toString());
1544 qtype
= stringFromJson(rrset
, "type");
1545 if (qtype
.getCode() == 0) {
1546 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1548 if (rrset
["records"].is_array()) {
1549 int ttl
= intFromJson(rrset
, "ttl");
1550 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1552 if (rrset
["comments"].is_array()) {
1553 gatherComments(rrset
, qname
, qtype
, new_comments
);
1556 } else if (zonestring
!= "") {
1557 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1560 for(auto& rr
: new_records
) {
1561 rr
.qname
.makeUsLowerCase();
1562 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1563 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1564 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1566 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1568 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1570 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1571 have_zone_ns
= true;
1575 // synthesize RRs as needed
1576 DNSResourceRecord autorr
;
1577 autorr
.qname
= zonename
;
1579 autorr
.ttl
= ::arg().asNum("default-ttl");
1581 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1582 // synthesize a SOA record so the zone "really" exists
1583 string soa
= (boost::format("%s %s %ul")
1584 % ::arg()["default-soa-name"]
1585 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1586 % document
["serial"].int_value()
1589 fillSOAData(soa
, sd
); // fills out default values for us
1590 autorr
.qtype
= QType::SOA
;
1591 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1592 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1593 new_records
.push_back(autorr
);
1596 // create NS records if nameservers are given
1597 for (auto value
: nameservers
.array_items()) {
1598 string nameserver
= value
.string_value();
1599 if (nameserver
.empty())
1600 throw ApiException("Nameservers must be non-empty strings");
1601 if (!isCanonical(nameserver
))
1602 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1604 // ensure the name parses
1605 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1607 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1609 autorr
.qtype
= QType::NS
;
1610 new_records
.push_back(autorr
);
1612 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1616 checkNewRecords(new_records
);
1618 if (boolFromJson(document
, "dnssec", false)) {
1619 checkDefaultDNSSECAlgos();
1621 if(document
["nsec3param"].string_value().length() > 0) {
1622 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1623 string error_msg
= "";
1624 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1625 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1630 // no going back after this
1631 if(!B
.createDomain(zonename
))
1632 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1634 if(!B
.getDomainInfo(zonename
, di
))
1635 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1637 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1638 if (!soa_edit_api_kind
.empty()) {
1639 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1642 di
.backend
->startTransaction(zonename
, di
.id
);
1644 for(auto rr
: new_records
) {
1645 rr
.domain_id
= di
.id
;
1646 di
.backend
->feedRecord(rr
, DNSName());
1648 for(Comment
& c
: new_comments
) {
1649 c
.domain_id
= di
.id
;
1650 di
.backend
->feedComment(c
);
1653 updateDomainSettingsFromDocument(B
, di
, zonename
, document
);
1655 di
.backend
->commitTransaction();
1657 storeChangedPTRs(B
, new_ptrs
);
1659 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1664 if(req
->method
!= "GET")
1665 throw HttpMethodNotAllowedException();
1667 vector
<DomainInfo
> domains
;
1669 if (req
->getvars
.count("zone")) {
1670 string zone
= req
->getvars
["zone"];
1671 apiCheckNameAllowedCharacters(zone
);
1672 DNSName zonename
= apiNameToDNSName(zone
);
1673 zonename
.makeUsLowerCase();
1675 if (B
.getDomainInfo(zonename
, di
)) {
1676 domains
.push_back(di
);
1679 B
.getAllDomains(&domains
, true); // incl. disabled
1683 for(const DomainInfo
& di
: domains
) {
1684 doc
.push_back(getZoneInfo(di
, &dk
));
1689 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1690 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1694 if (!B
.getDomainInfo(zonename
, di
)) {
1695 throw HttpNotFoundException();
1698 if(req
->method
== "PUT") {
1699 // update domain settings
1701 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json());
1704 resp
->status
= 204; // No Content, but indicate success
1707 else if(req
->method
== "DELETE") {
1709 if(!di
.backend
->deleteDomain(zonename
))
1710 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1713 DNSSECKeeper
dk(&B
);
1714 dk
.clearCaches(zonename
);
1715 purgeAuthCaches(zonename
.toString() + "$");
1717 // empty body on success
1719 resp
->status
= 204; // No Content: declare that the zone is gone now
1721 } else if (req
->method
== "PATCH") {
1722 patchZone(req
, resp
);
1724 } else if (req
->method
== "GET") {
1725 fillZone(zonename
, resp
, shouldDoRRSets(req
));
1728 throw HttpMethodNotAllowedException();
1731 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1732 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1734 if(req
->method
!= "GET")
1735 throw HttpMethodNotAllowedException();
1741 if (!B
.getDomainInfo(zonename
, di
)) {
1742 throw HttpNotFoundException();
1745 DNSResourceRecord rr
;
1747 di
.backend
->list(zonename
, di
.id
);
1748 while(di
.backend
->get(rr
)) {
1749 if (!rr
.qtype
.getCode())
1750 continue; // skip empty non-terminals
1753 rr
.qname
.toString() << "\t" <<
1756 rr
.qtype
.getName() << "\t" <<
1757 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1761 if (req
->accept_json
) {
1762 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1764 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1765 resp
->body
= ss
.str();
1769 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
) {
1770 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1772 if(req
->method
!= "PUT")
1773 throw HttpMethodNotAllowedException();
1777 if (!B
.getDomainInfo(zonename
, di
)) {
1778 throw HttpNotFoundException();
1781 if(di
.masters
.empty())
1782 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1784 random_shuffle(di
.masters
.begin(), di
.masters
.end());
1785 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1786 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1789 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1790 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1792 if(req
->method
!= "PUT")
1793 throw HttpMethodNotAllowedException();
1797 if (!B
.getDomainInfo(zonename
, di
)) {
1798 throw HttpNotFoundException();
1801 if(!Communicator
.notifyDomain(zonename
))
1802 throw ApiException("Failed to add to the queue - see server log");
1804 resp
->setSuccessResult("Notification queued");
1807 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1808 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1810 if(req
->method
!= "PUT")
1811 throw HttpMethodNotAllowedException();
1815 if (!B
.getDomainInfo(zonename
, di
)) {
1816 throw HttpNotFoundException();
1819 DNSSECKeeper
dk(&B
);
1821 if (!dk
.isSecuredZone(zonename
))
1822 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1824 if (di
.kind
== DomainInfo::Slave
)
1825 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1827 string error_msg
= "";
1829 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1830 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1832 resp
->setSuccessResult("Rectified");
1835 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1836 if (rr
.qtype
.getCode() == QType::A
) {
1838 if (!IpToU32(rr
.content
, &ip
)) {
1839 throw ApiException("PTR: Invalid IP address given");
1841 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1842 % ((ip
>> 24) & 0xff)
1843 % ((ip
>> 16) & 0xff)
1844 % ((ip
>> 8) & 0xff)
1847 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1848 ComboAddress
ca(rr
.content
);
1851 for (int octet
= 0; octet
< 16; ++octet
) {
1852 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1853 // this should be impossible: no byte should give more than two digits in hex format
1854 throw PDNSException("Formatting IPv6 address failed");
1856 ss
<< buf
[0] << '.' << buf
[1] << '.';
1858 string tmp
= ss
.str();
1859 tmp
.resize(tmp
.size()-1); // remove last dot
1860 // reverse and append arpa domain
1861 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1863 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1868 ptr
->disabled
= rr
.disabled
;
1869 ptr
->content
= rr
.qname
.toStringRootDot();
1872 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1873 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1875 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1876 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1878 string soa_edit_api_kind
;
1879 string soa_edit_kind
;
1880 bool soa_changed
= false;
1881 DNSResourceRecord soarr
;
1882 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1883 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1884 if (!soa_edit_api_kind
.empty()) {
1885 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1888 sd
.db
->startTransaction(sd
.qname
);
1889 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1890 sd
.db
->abortTransaction();
1891 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1895 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1898 sd
.db
->commitTransaction();
1899 purgeAuthCachesExact(rr
.qname
);
1903 static void patchZone(HttpRequest
* req
, HttpResponse
* resp
) {
1906 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1907 if (!B
.getDomainInfo(zonename
, di
)) {
1908 throw HttpNotFoundException();
1911 vector
<DNSResourceRecord
> new_records
;
1912 vector
<Comment
> new_comments
;
1913 vector
<DNSResourceRecord
> new_ptrs
;
1915 Json document
= req
->json();
1917 auto rrsets
= document
["rrsets"];
1918 if (!rrsets
.is_array())
1919 throw ApiException("No rrsets given in update request");
1921 di
.backend
->startTransaction(zonename
);
1924 string soa_edit_api_kind
;
1925 string soa_edit_kind
;
1926 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1927 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1928 bool soa_edit_done
= false;
1930 set
<pair
<DNSName
, QType
>> seen
;
1932 for (const auto& rrset
: rrsets
.array_items()) {
1933 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1934 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1935 apiCheckQNameAllowedCharacters(qname
.toString());
1937 qtype
= stringFromJson(rrset
, "type");
1938 if (qtype
.getCode() == 0) {
1939 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1942 if(seen
.count({qname
, qtype
}))
1944 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1946 seen
.insert({qname
, qtype
});
1948 if (changetype
== "DELETE") {
1949 // delete all matching qname/qtype RRs (and, implicitly comments).
1950 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1951 throw ApiException("Hosting backend does not support editing records.");
1954 else if (changetype
== "REPLACE") {
1955 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1956 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
1957 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
1959 bool replace_records
= rrset
["records"].is_array();
1960 bool replace_comments
= rrset
["comments"].is_array();
1962 if (!replace_records
&& !replace_comments
) {
1963 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
1966 new_records
.clear();
1967 new_comments
.clear();
1969 if (replace_records
) {
1970 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
1971 int ttl
= intFromJson(rrset
, "ttl");
1972 // new_ptrs is merged.
1973 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1975 for(DNSResourceRecord
& rr
: new_records
) {
1976 rr
.domain_id
= di
.id
;
1977 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1978 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1981 checkNewRecords(new_records
);
1984 if (replace_comments
) {
1985 gatherComments(rrset
, qname
, qtype
, new_comments
);
1987 for(Comment
& c
: new_comments
) {
1988 c
.domain_id
= di
.id
;
1992 if (replace_records
) {
1993 bool ent_present
= false;
1994 di
.backend
->lookup(QType(QType::ANY
), qname
);
1995 DNSResourceRecord rr
;
1996 while (di
.backend
->get(rr
)) {
1997 if (qtype
.getCode() == 0) {
2000 if (qtype
.getCode() != rr
.qtype
.getCode()
2001 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2002 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2003 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2007 if (!new_records
.empty() && ent_present
) {
2009 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2010 throw ApiException("Hosting backend does not support editing records.");
2013 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2014 throw ApiException("Hosting backend does not support editing records.");
2017 if (replace_comments
) {
2018 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2019 throw ApiException("Hosting backend does not support editing comments.");
2024 throw ApiException("Changetype not understood");
2027 // edit SOA (if needed)
2028 if (!soa_edit_api_kind
.empty() && !soa_edit_done
) {
2030 if (!B
.getSOAUncached(zonename
, sd
))
2031 throw ApiException("No SOA found for domain '"+zonename
.toString()+"'");
2033 DNSResourceRecord rr
;
2034 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2035 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2036 throw ApiException("Hosting backend does not support editing records.");
2040 // return old and new serials in headers
2041 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2042 fillSOAData(rr
.content
, sd
);
2043 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2047 di
.backend
->abortTransaction();
2051 DNSSECKeeper
dk(&B
);
2053 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
2054 if (dk
.isSecuredZone(zonename
) && !dk
.isPresigned(zonename
) && api_rectify
== "1") {
2055 string error_msg
= "";
2057 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false))
2058 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2061 di
.backend
->commitTransaction();
2063 purgeAuthCachesExact(zonename
);
2066 storeChangedPTRs(B
, new_ptrs
);
2069 resp
->status
= 204; // No Content, but indicate success
2073 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2074 if(req
->method
!= "GET")
2075 throw HttpMethodNotAllowedException();
2077 string q
= req
->getvars
["q"];
2078 string sMax
= req
->getvars
["max"];
2079 string sObjectType
= req
->getvars
["object_type"];
2084 // the following types of data can be searched for using the api
2085 enum class ObjectType
2094 throw ApiException("Query q can't be blank");
2096 maxEnts
= std::stoi(sMax
);
2098 throw ApiException("Maximum entries must be larger than 0");
2100 if (sObjectType
.empty())
2101 objectType
= ObjectType::ALL
;
2102 else if (sObjectType
== "all")
2103 objectType
= ObjectType::ALL
;
2104 else if (sObjectType
== "zone")
2105 objectType
= ObjectType::ZONE
;
2106 else if (sObjectType
== "record")
2107 objectType
= ObjectType::RECORD
;
2108 else if (sObjectType
== "comment")
2109 objectType
= ObjectType::COMMENT
;
2111 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2113 SimpleMatch
sm(q
,true);
2115 vector
<DomainInfo
> domains
;
2116 vector
<DNSResourceRecord
> result_rr
;
2117 vector
<Comment
> result_c
;
2118 map
<int,DomainInfo
> zoneIdZone
;
2119 map
<int,DomainInfo
>::iterator val
;
2122 B
.getAllDomains(&domains
, true);
2124 for(const DomainInfo di
: domains
)
2126 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2127 doc
.push_back(Json::object
{
2128 { "object_type", "zone" },
2129 { "zone_id", apiZoneNameToId(di
.zone
) },
2130 { "name", di
.zone
.toString() }
2134 zoneIdZone
[di
.id
] = di
; // populate cache
2137 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2139 for(const DNSResourceRecord
& rr
: result_rr
)
2141 if (!rr
.qtype
.getCode())
2142 continue; // skip empty non-terminals
2144 auto object
= Json::object
{
2145 { "object_type", "record" },
2146 { "name", rr
.qname
.toString() },
2147 { "type", rr
.qtype
.getName() },
2148 { "ttl", (double)rr
.ttl
},
2149 { "disabled", rr
.disabled
},
2150 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2152 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2153 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2154 object
["zone"] = val
->second
.zone
.toString();
2156 doc
.push_back(object
);
2160 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2162 for(const Comment
&c
: result_c
)
2164 auto object
= Json::object
{
2165 { "object_type", "comment" },
2166 { "name", c
.qname
.toString() },
2167 { "content", c
.content
}
2169 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2170 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2171 object
["zone"] = val
->second
.zone
.toString();
2173 doc
.push_back(object
);
2180 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2181 if(req
->method
!= "PUT")
2182 throw HttpMethodNotAllowedException();
2184 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2186 uint64_t count
= purgeAuthCachesExact(canon
);
2187 resp
->setBody(Json::object
{
2188 { "count", (int) count
},
2189 { "result", "Flushed cache." }
2193 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2195 resp
->headers
["Cache-Control"] = "max-age=86400";
2196 resp
->headers
["Content-Type"] = "text/css";
2199 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2200 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2201 ret
<<"a { color: #0959c2; }"<<endl
;
2202 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2203 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2204 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2205 ret
<<".row:after { clear: both; }"<<endl
;
2206 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2207 ret
<<".all { width: 100%; }"<<endl
;
2208 ret
<<".headl { width: 60%; }"<<endl
;
2209 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2210 ret
<<"background-image: url(data:image/png;base64,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);";
2211 ret
<<" width: 154px; height: 20px; }"<<endl
;
2212 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2213 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2214 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2215 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2216 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2217 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2218 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2219 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2220 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2221 ret
<<".resetring {float: right; }"<<endl
;
2222 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
;
2223 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2224 ret
<<".resizering {float: right;}"<<endl
;
2225 resp
->body
= ret
.str();
2229 void AuthWebServer::webThread()
2232 setThreadName("pdns/webserver");
2233 if(::arg().mustDo("api")) {
2234 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2235 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2236 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2237 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2238 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2239 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2240 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2241 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2242 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2243 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2244 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2245 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2246 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2247 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2248 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2249 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2250 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2251 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2252 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2254 if (::arg().mustDo("webserver")) {
2255 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2256 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2261 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;