2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 #include "dynlistener.hh"
29 #include "webserver.hh"
34 #include "arguments.hh"
37 #include "ueberbackend.hh"
38 #include <boost/format.hpp>
40 #include "namespaces.hh"
43 #include "dnsseckeeper.hh"
45 #include "zoneparser-tng.hh"
46 #include "common_startup.hh"
47 #include "auth-caches.hh"
48 #include "threadname.hh"
49 #include "tsigutils.hh"
55 static void patchZone(UeberBackend
& B
, HttpRequest
* req
, HttpResponse
* resp
);
56 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
);
57 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
);
59 // QTypes that MUST NOT have multiple records of the same type in a given RRset.
60 static const std::set
<uint16_t> onlyOneEntryTypes
= { QType::CNAME
, QType::DNAME
, QType::SOA
};
61 // QTypes that MUST NOT be used with any other QType on the same name.
62 static const std::set
<uint16_t> exclusiveEntryTypes
= { QType::CNAME
, 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"]);
81 d_ws
->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
87 void AuthWebServer::go()
90 pthread_create(&d_tid
, 0, webThreadHelper
, this);
91 pthread_create(&d_tid
, 0, statThreadHelper
, this);
94 void AuthWebServer::statThread()
97 setThreadName("pdns/statHelper");
99 d_queries
.submit(S
.read("udp-queries"));
100 d_cachehits
.submit(S
.read("packetcache-hit"));
101 d_cachemisses
.submit(S
.read("packetcache-miss"));
102 d_qcachehits
.submit(S
.read("query-cache-hit"));
103 d_qcachemisses
.submit(S
.read("query-cache-miss"));
108 g_log
<<Logger::Error
<<"Webserver statThread caught an exception, dying"<<endl
;
113 void *AuthWebServer::statThreadHelper(void *p
)
115 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
117 return 0; // never reached
120 void *AuthWebServer::webThreadHelper(void *p
)
122 AuthWebServer
*self
=static_cast<AuthWebServer
*>(p
);
124 return 0; // never reached
127 static string
htmlescape(const string
&s
) {
129 for(string::const_iterator it
=s
.begin(); it
!=s
.end(); ++it
) {
150 void printtable(ostringstream
&ret
, const string
&ringname
, const string
&title
, int limit
=10)
154 vector
<pair
<string
,unsigned int> >ring
=S
.getRing(ringname
);
156 for(vector
<pair
<string
, unsigned int> >::const_iterator i
=ring
.begin(); i
!=ring
.end();++i
) {
161 ret
<<"<div class=\"panel\">";
162 ret
<<"<span class=resetring><i></i><a href=\"?resetring="<<htmlescape(ringname
)<<"\">Reset</a></span>"<<endl
;
163 ret
<<"<h2>"<<title
<<"</h2>"<<endl
;
164 ret
<<"<div class=ringmeta>";
165 ret
<<"<a class=topXofY href=\"?ring="<<htmlescape(ringname
)<<"\">Showing: Top "<<limit
<<" of "<<entries
<<"</a>"<<endl
;
166 ret
<<"<span class=resizering>Resize: ";
167 unsigned int sizes
[]={10,100,500,1000,10000,500000,0};
168 for(int i
=0;sizes
[i
];++i
) {
169 if(S
.getRingSize(ringname
)!=sizes
[i
])
170 ret
<<"<a href=\"?resizering="<<htmlescape(ringname
)<<"&size="<<sizes
[i
]<<"\">"<<sizes
[i
]<<"</a> ";
172 ret
<<"("<<sizes
[i
]<<") ";
174 ret
<<"</span></div>";
176 ret
<<"<table class=\"data\">";
178 int total
=max(1,tot
);
179 for(vector
<pair
<string
,unsigned int> >::const_iterator i
=ring
.begin();limit
&& i
!=ring
.end();++i
,--limit
) {
180 ret
<<"<tr><td>"<<htmlescape(i
->first
)<<"</td><td>"<<i
->second
<<"</td><td align=right>"<< AuthWebServer::makePercentage(i
->second
*100.0/total
)<<"</td>"<<endl
;
183 ret
<<"<tr><td colspan=3></td></tr>"<<endl
;
185 ret
<<"<tr><td><b>Rest:</b></td><td><b>"<<tot
-printed
<<"</b></td><td align=right><b>"<< AuthWebServer::makePercentage((tot
-printed
)*100.0/total
)<<"</b></td>"<<endl
;
187 ret
<<"<tr><td><b>Total:</b></td><td><b>"<<tot
<<"</b></td><td align=right><b>100%</b></td>";
188 ret
<<"</table></div>"<<endl
;
191 void AuthWebServer::printvars(ostringstream
&ret
)
193 ret
<<"<div class=panel><h2>Variables</h2><table class=\"data\">"<<endl
;
195 vector
<string
>entries
=S
.getEntries();
196 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
197 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<S
.read(*i
)<<"</td><td>"<<S
.getDescrip(*i
)<<"</td>"<<endl
;
200 ret
<<"</table></div>"<<endl
;
203 void AuthWebServer::printargs(ostringstream
&ret
)
205 ret
<<"<table border=1><tr><td colspan=3 bgcolor=\"#0000ff\"><font color=\"#ffffff\">Arguments</font></td>"<<endl
;
207 vector
<string
>entries
=arg().list();
208 for(vector
<string
>::const_iterator i
=entries
.begin();i
!=entries
.end();++i
) {
209 ret
<<"<tr><td>"<<*i
<<"</td><td>"<<arg()[*i
]<<"</td><td>"<<arg().getHelp(*i
)<<"</td>"<<endl
;
213 string
AuthWebServer::makePercentage(const double& val
)
215 return (boost::format("%.01f%%") % val
).str();
218 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
220 if(!req
->getvars
["resetring"].empty()) {
221 if (S
.ringExists(req
->getvars
["resetring"]))
222 S
.resetRing(req
->getvars
["resetring"]);
224 resp
->headers
["Location"] = req
->url
.path
;
227 if(!req
->getvars
["resizering"].empty()){
228 int size
=std::stoi(req
->getvars
["size"]);
229 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000)
230 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
232 resp
->headers
["Location"] = req
->url
.path
;
238 ret
<<"<!DOCTYPE html>"<<endl
;
239 ret
<<"<html><head>"<<endl
;
240 ret
<<"<title>PowerDNS Authoritative Server Monitor</title>"<<endl
;
241 ret
<<"<link rel=\"stylesheet\" href=\"style.css\"/>"<<endl
;
242 ret
<<"</head><body>"<<endl
;
244 ret
<<"<div class=\"row\">"<<endl
;
245 ret
<<"<div class=\"headl columns\">";
246 ret
<<"<a href=\"/\" id=\"appname\">PowerDNS "<<htmlescape(VERSION
);
247 if(!arg()["config-name"].empty()) {
248 ret
<<" ["<<htmlescape(arg()["config-name"])<<"]";
250 ret
<<"</a></div>"<<endl
;
251 ret
<<"<div class=\"headr columns\"></div></div>";
252 ret
<<"<div class=\"row\"><div class=\"all columns\">";
254 time_t passed
=time(0)-s_starttime
;
257 humanDuration(passed
)<<
260 ret
<<"Queries/second, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
261 (int)d_queries
.get1()<<", "<<
262 (int)d_queries
.get5()<<", "<<
263 (int)d_queries
.get10()<<". Max queries/second: "<<(int)d_queries
.getMax()<<
266 if(d_cachemisses
.get10()+d_cachehits
.get10()>0)
267 ret
<<"Cache hitrate, 1, 5, 10 minute averages: "<<
268 makePercentage((d_cachehits
.get1()*100.0)/((d_cachehits
.get1())+(d_cachemisses
.get1())))<<", "<<
269 makePercentage((d_cachehits
.get5()*100.0)/((d_cachehits
.get5())+(d_cachemisses
.get5())))<<", "<<
270 makePercentage((d_cachehits
.get10()*100.0)/((d_cachehits
.get10())+(d_cachemisses
.get10())))<<
273 if(d_qcachemisses
.get10()+d_qcachehits
.get10()>0)
274 ret
<<"Backend query cache hitrate, 1, 5, 10 minute averages: "<<std::setprecision(2)<<
275 makePercentage((d_qcachehits
.get1()*100.0)/((d_qcachehits
.get1())+(d_qcachemisses
.get1())))<<", "<<
276 makePercentage((d_qcachehits
.get5()*100.0)/((d_qcachehits
.get5())+(d_qcachemisses
.get5())))<<", "<<
277 makePercentage((d_qcachehits
.get10()*100.0)/((d_qcachehits
.get10())+(d_qcachemisses
.get10())))<<
280 ret
<<"Backend query load, 1, 5, 10 minute averages: "<<std::setprecision(3)<<
281 (int)d_qcachemisses
.get1()<<", "<<
282 (int)d_qcachemisses
.get5()<<", "<<
283 (int)d_qcachemisses
.get10()<<". Max queries/second: "<<(int)d_qcachemisses
.getMax()<<
286 ret
<<"Total queries: "<<S
.read("udp-queries")<<". Question/answer latency: "<<S
.read("latency")/1000.0<<"ms</p><br>"<<endl
;
287 if(req
->getvars
["ring"].empty()) {
288 auto entries
= S
.listRings();
289 for(const auto &i
: entries
) {
290 printtable(ret
, i
, S
.getRingTitle(i
));
294 if(arg().mustDo("webserver-print-arguments"))
297 else if(S
.ringExists(req
->getvars
["ring"]))
298 printtable(ret
,req
->getvars
["ring"],S
.getRingTitle(req
->getvars
["ring"]),100);
300 ret
<<"</div></div>"<<endl
;
301 ret
<<"<footer class=\"row\">"<<fullVersionString()<<"<br>© 2013 - 2019 <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>"<<endl
;
302 ret
<<"</body></html>"<<endl
;
304 resp
->body
= ret
.str();
308 /** Helper to build a record content as needed. */
309 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
) {
310 // noDot: for backend storage, pass true. for API users, pass false.
311 auto drc
= DNSRecordContent::mastermake(qtype
.getCode(), QClass::IN
, content
);
312 return drc
->getZoneRepresentation(noDot
);
315 /** "Normalize" record content for API consumers. */
316 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
) {
317 return makeRecordContent(qtype
, content
, false);
320 /** "Normalize" record content for backend storage. */
321 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
) {
322 return makeRecordContent(qtype
, content
, true);
325 static Json::object
getZoneInfo(const DomainInfo
& di
, DNSSECKeeper
* dk
) {
326 string zoneId
= apiZoneNameToId(di
.zone
);
327 vector
<string
> masters
;
328 for(const auto& m
: di
.masters
)
329 masters
.push_back(m
.toStringWithPortExcept(53));
331 auto obj
= Json::object
{
332 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
334 { "url", "/api/v1/servers/localhost/zones/" + zoneId
},
335 { "name", di
.zone
.toString() },
336 { "kind", di
.getKindString() },
337 { "account", di
.account
},
338 { "masters", masters
},
339 { "serial", (double)di
.serial
},
340 { "notified_serial", (double)di
.notified_serial
},
341 { "last_check", (double)di
.last_check
}
344 obj
["dnssec"] = dk
->isSecuredZone(di
.zone
);
345 obj
["edited_serial"] = (double)calculateEditSOA(di
.serial
, *dk
, di
.zone
);
350 static bool shouldDoRRSets(HttpRequest
* req
) {
351 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true")
353 if (req
->getvars
["rrsets"] == "false")
355 throw ApiException("'rrsets' request parameter value '"+req
->getvars
["rrsets"]+"' is not supported");
358 static void fillZone(UeberBackend
& B
, const DNSName
& zonename
, HttpResponse
* resp
, bool doRRSets
) {
360 if(!B
.getDomainInfo(zonename
, di
)) {
361 throw HttpNotFoundException();
365 Json::object doc
= getZoneInfo(di
, &dk
);
366 // extra stuff getZoneInfo doesn't do for us (more expensive)
368 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
369 doc
["soa_edit_api"] = soa_edit_api
;
371 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
372 doc
["soa_edit"] = soa_edit
;
374 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
375 doc
["nsec3param"] = nsec3param
;
377 bool nsec3narrowbool
= false;
378 di
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
379 if (nsec3narrow
== "1")
380 nsec3narrowbool
= true;
381 doc
["nsec3narrow"] = nsec3narrowbool
;
382 doc
["dnssec"] = dk
.isSecuredZone(zonename
);
385 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
386 doc
["api_rectify"] = (api_rectify
== "1");
389 vector
<string
> tsig_master
, tsig_slave
;
390 di
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_master
);
391 di
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_slave
);
393 Json::array tsig_master_keys
;
394 for (const auto& keyname
: tsig_master
) {
395 tsig_master_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
397 doc
["master_tsig_key_ids"] = tsig_master_keys
;
399 Json::array tsig_slave_keys
;
400 for (const auto& keyname
: tsig_slave
) {
401 tsig_slave_keys
.push_back(apiZoneNameToId(DNSName(keyname
)));
403 doc
["slave_tsig_key_ids"] = tsig_slave_keys
;
406 vector
<DNSResourceRecord
> records
;
407 vector
<Comment
> comments
;
409 // load all records + sort
411 DNSResourceRecord rr
;
412 di
.backend
->list(zonename
, di
.id
, true); // incl. disabled
413 while(di
.backend
->get(rr
)) {
414 if (!rr
.qtype
.getCode())
415 continue; // skip empty non-terminals
416 records
.push_back(rr
);
418 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& a
, const DNSResourceRecord
& b
) {
419 /* if you ever want to update this comparison function,
420 please be aware that you will also need to update the conditions in the code merging
421 the records and comments below */
422 if (a
.qname
== b
.qname
) {
423 return b
.qtype
< a
.qtype
;
425 return b
.qname
< a
.qname
;
429 // load all comments + sort
432 di
.backend
->listComments(di
.id
);
433 while(di
.backend
->getComment(comment
)) {
434 comments
.push_back(comment
);
436 sort(comments
.begin(), comments
.end(), [](const Comment
& a
, const Comment
& b
) {
437 /* if you ever want to update this comparison function,
438 please be aware that you will also need to update the conditions in the code merging
439 the records and comments below */
440 if (a
.qname
== b
.qname
) {
441 return b
.qtype
< a
.qtype
;
443 return b
.qname
< a
.qname
;
449 Json::array rrset_records
;
450 Json::array rrset_comments
;
451 DNSName current_qname
;
454 auto rit
= records
.begin();
455 auto cit
= comments
.begin();
457 while (rit
!= records
.end() || cit
!= comments
.end()) {
458 // 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
459 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
460 current_qname
= rit
->qname
;
461 current_qtype
= rit
->qtype
;
464 current_qname
= cit
->qname
;
465 current_qtype
= cit
->qtype
;
469 while(rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
470 ttl
= min(ttl
, rit
->ttl
);
471 rrset_records
.push_back(Json::object
{
472 { "disabled", rit
->disabled
},
473 { "content", makeApiRecordContent(rit
->qtype
, rit
->content
) }
477 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
478 rrset_comments
.push_back(Json::object
{
479 { "modified_at", (double)cit
->modified_at
},
480 { "account", cit
->account
},
481 { "content", cit
->content
}
486 rrset
["name"] = current_qname
.toString();
487 rrset
["type"] = current_qtype
.getName();
488 rrset
["records"] = rrset_records
;
489 rrset
["comments"] = rrset_comments
;
490 rrset
["ttl"] = (double)ttl
;
491 rrsets
.push_back(rrset
);
493 rrset_records
.clear();
494 rrset_comments
.clear();
497 doc
["rrsets"] = rrsets
;
503 void productServerStatisticsFetch(map
<string
,string
>& out
)
505 vector
<string
> items
= S
.getEntries();
506 for(const string
& item
: items
) {
507 out
[item
] = std::to_string(S
.read(item
));
511 out
["uptime"] = std::to_string(time(0) - s_starttime
);
514 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
517 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
525 static void validateGatheredRRType(const DNSResourceRecord
& rr
) {
526 if (rr
.qtype
.getCode() == QType::OPT
|| rr
.qtype
.getCode() == QType::TSIG
) {
527 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": invalid type given");
531 static void gatherRecords(UeberBackend
& B
, const string
& logprefix
, const Json container
, const DNSName
& qname
, const QType qtype
, const int ttl
, vector
<DNSResourceRecord
>& new_records
, vector
<DNSResourceRecord
>& new_ptrs
) {
532 DNSResourceRecord rr
;
538 validateGatheredRRType(rr
);
539 const auto& items
= container
["records"].array_items();
540 for(const auto& record
: items
) {
541 string content
= stringFromJson(record
, "content");
543 if(!record
["disabled"].is_null()) {
544 rr
.disabled
= boolFromJson(record
, "disabled");
547 // validate that the client sent something we can actually parse, and require that data to be dotted.
549 if (rr
.qtype
.getCode() != QType::AAAA
) {
550 string tmp
= makeApiRecordContent(rr
.qtype
, content
);
551 if (!pdns_iequals(tmp
, content
)) {
552 throw std::runtime_error("Not in expected format (parsed as '"+tmp
+"')");
555 struct in6_addr tmpbuf
;
556 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
557 throw std::runtime_error("Invalid IPv6 address");
560 rr
.content
= makeBackendRecordContent(rr
.qtype
, content
);
562 catch(std::exception
& e
)
564 throw ApiException("Record "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" '"+content
+"': "+e
.what());
567 if ((rr
.qtype
.getCode() == QType::A
|| rr
.qtype
.getCode() == QType::AAAA
) &&
568 boolFromJson(record
, "set-ptr", false) == true) {
570 g_log
<<Logger::Warning
<<logprefix
<<"API call uses deprecated set-ptr feature, please remove it"<<endl
;
572 DNSResourceRecord ptr
;
575 // verify that there's a zone for the PTR
577 if (!B
.getAuth(ptr
.qname
, QType(QType::PTR
), &sd
, false))
578 throw ApiException("Could not find domain for PTR '"+ptr
.qname
.toString()+"' requested for '"+ptr
.content
+"'");
580 ptr
.domain_id
= sd
.domain_id
;
581 new_ptrs
.push_back(ptr
);
584 new_records
.push_back(rr
);
588 static void gatherComments(const Json container
, const DNSName
& qname
, const QType qtype
, vector
<Comment
>& new_comments
) {
593 time_t now
= time(0);
594 for (auto comment
: container
["comments"].array_items()) {
595 c
.modified_at
= intFromJson(comment
, "modified_at", now
);
596 c
.content
= stringFromJson(comment
, "content");
597 c
.account
= stringFromJson(comment
, "account");
598 new_comments
.push_back(c
);
602 static void checkDefaultDNSSECAlgos() {
603 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
604 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
605 int k_size
= arg().asNum("default-ksk-size");
606 int z_size
= arg().asNum("default-zsk-size");
608 // Sanity check DNSSEC parameters
609 if (::arg()["default-zsk-algorithm"] != "") {
611 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
612 else if (k_algo
<= 10 && k_size
== 0)
613 throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
616 if (::arg()["default-zsk-algorithm"] != "") {
618 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
619 else if (z_algo
<= 10 && z_size
== 0)
620 throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
624 static void throwUnableToSecure(const DNSName
& zonename
) {
625 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
626 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
629 static void updateDomainSettingsFromDocument(UeberBackend
& B
, const DomainInfo
& di
, const DNSName
& zonename
, const Json document
, bool rectifyTransaction
=true) {
630 vector
<string
> zonemaster
;
631 bool shouldRectify
= false;
632 for(auto value
: document
["masters"].array_items()) {
633 string master
= value
.string_value();
635 throw ApiException("Master can not be an empty string");
637 ComboAddress
m(master
);
638 } catch (const PDNSException
&e
) {
639 throw ApiException("Master (" + master
+ ") is not an IP address: " + e
.reason
);
641 zonemaster
.push_back(master
);
644 if (zonemaster
.size()) {
645 di
.backend
->setMaster(zonename
, boost::join(zonemaster
, ","));
647 if (document
["kind"].is_string()) {
648 di
.backend
->setKind(zonename
, DomainInfo::stringToKind(stringFromJson(document
, "kind")));
650 if (document
["soa_edit_api"].is_string()) {
651 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
653 if (document
["soa_edit"].is_string()) {
654 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
657 bool api_rectify
= boolFromJson(document
, "api_rectify");
658 di
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
660 catch (const JsonException
&) {}
662 if (document
["account"].is_string()) {
663 di
.backend
->setAccount(zonename
, document
["account"].string_value());
667 bool dnssecInJSON
= false;
668 bool dnssecDocVal
= false;
671 dnssecDocVal
= boolFromJson(document
, "dnssec");
674 catch (const JsonException
&) {}
676 bool isDNSSECZone
= dk
.isSecuredZone(zonename
);
681 checkDefaultDNSSECAlgos();
683 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
684 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
685 int k_size
= arg().asNum("default-ksk-size");
686 int z_size
= arg().asNum("default-zsk-size");
690 if (!dk
.addKey(zonename
, true, k_algo
, id
, k_size
)) {
691 throwUnableToSecure(zonename
);
697 if (!dk
.addKey(zonename
, false, z_algo
, id
, z_size
)) {
698 throwUnableToSecure(zonename
);
702 // Used later for NSEC3PARAM
703 isDNSSECZone
= dk
.isSecuredZone(zonename
);
706 throwUnableToSecure(zonename
);
708 shouldRectify
= true;
711 // "dnssec": false in json
714 if (!dk
.unSecureZone(zonename
, error
, info
)) {
715 throw ApiException("Error while un-securing zone '"+ zonename
.toString()+"': " + error
);
717 isDNSSECZone
= dk
.isSecuredZone(zonename
);
719 throw ApiException("Unable to un-secure zone '"+ zonename
.toString()+"'");
721 shouldRectify
= true;
726 if(document
["nsec3param"].string_value().length() > 0) {
727 shouldRectify
= true;
728 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
729 string error_msg
= "";
731 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"', but zone is not DNSSEC secured.");
733 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
734 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
736 if (!dk
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
737 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() +
738 "' passed our basic sanity checks, but cannot be used with the current backend.");
742 if (shouldRectify
&& !dk
.isPresigned(zonename
)) {
745 di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
746 if (api_rectify
.empty()) {
747 if (::arg().mustDo("default-api-rectify")) {
751 if (api_rectify
== "1") {
754 if (!dk
.rectifyZone(zonename
, error_msg
, info
, rectifyTransaction
)) {
755 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
760 string soa_edit_api_kind
;
761 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
762 if (!soa_edit_api_kind
.empty()) {
764 if (!B
.getSOAUncached(zonename
, sd
))
767 string soa_edit_kind
;
768 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
770 DNSResourceRecord rr
;
771 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
772 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
773 throw ApiException("Hosting backend does not support editing records.");
779 if (!document
["master_tsig_key_ids"].is_null()) {
780 vector
<string
> metadata
;
783 for(auto value
: document
["master_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
, "TSIG-ALLOW-AXFR", metadata
)) {
792 throw HttpInternalServerErrorException("Unable to set new TSIG master keys for zone '" + zonename
.toLogString() + "'");
795 if (!document
["slave_tsig_key_ids"].is_null()) {
796 vector
<string
> metadata
;
799 for(auto value
: document
["slave_tsig_key_ids"].array_items()) {
800 auto keyname(apiZoneIdToName(value
.string_value()));
801 B
.getTSIGKey(keyname
, &keyAlgo
, &keyContent
);
802 if (keyAlgo
.empty() || keyContent
.empty()) {
803 throw ApiException("A TSIG key with the name '"+keyname
.toLogString()+"' does not exist");
805 metadata
.push_back(keyname
.toString());
807 if (!di
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
808 throw HttpInternalServerErrorException("Unable to set new TSIG slave keys for zone '" + zonename
.toLogString() + "'");
813 static bool isValidMetadataKind(const string
& kind
, bool readonly
) {
814 static vector
<string
> builtinOptions
{
817 "ALLOW-DNSUPDATE-FROM",
818 "TSIG-ALLOW-DNSUPDATE",
820 "SOA-EDIT-DNSUPDATE",
824 "GSS-ALLOW-AXFR-PRINCIPAL",
825 "GSS-ACCEPTOR-PRINCIPAL",
836 "TSIG-ALLOW-DNSUPDATE"
839 // the following options do not allow modifications via API
840 static vector
<string
> protectedOptions
{
850 if (kind
.find("X-") == 0)
855 for (const string
& s
: builtinOptions
) {
857 for (const string
& s2
: protectedOptions
) {
858 if (!readonly
&& s
== s2
)
869 static void apiZoneMetadata(HttpRequest
* req
, HttpResponse
*resp
) {
870 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
874 if (!B
.getDomainInfo(zonename
, di
)) {
875 throw HttpNotFoundException();
878 if (req
->method
== "GET") {
879 map
<string
, vector
<string
> > md
;
880 Json::array document
;
882 if (!B
.getAllDomainMetadata(zonename
, md
))
883 throw HttpNotFoundException();
885 for (const auto& i
: md
) {
887 for (string j
: i
.second
)
888 entries
.push_back(j
);
891 { "type", "Metadata" },
893 { "metadata", entries
}
896 document
.push_back(key
);
899 resp
->setBody(document
);
900 } else if (req
->method
== "POST") {
901 auto document
= req
->json();
903 vector
<string
> entries
;
906 kind
= stringFromJson(document
, "kind");
907 } catch (const JsonException
&) {
908 throw ApiException("kind is not specified or not a string");
911 if (!isValidMetadataKind(kind
, false))
912 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
914 vector
<string
> vecMetadata
;
916 if (!B
.getDomainMetadata(zonename
, kind
, vecMetadata
))
917 throw ApiException("Could not retrieve metadata entries for domain '" +
918 zonename
.toString() + "'");
920 auto& metadata
= document
["metadata"];
921 if (!metadata
.is_array())
922 throw ApiException("metadata is not specified or not an array");
924 for (const auto& i
: metadata
.array_items()) {
926 throw ApiException("metadata must be strings");
927 else if (std::find(vecMetadata
.cbegin(),
929 i
.string_value()) == vecMetadata
.cend()) {
930 vecMetadata
.push_back(i
.string_value());
934 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
935 throw ApiException("Could not update metadata entries for domain '" +
936 zonename
.toString() + "'");
938 Json::array respMetadata
;
939 for (const string
& s
: vecMetadata
)
940 respMetadata
.push_back(s
);
943 { "type", "Metadata" },
944 { "kind", document
["kind"] },
945 { "metadata", respMetadata
}
951 throw HttpMethodNotAllowedException();
954 static void apiZoneMetadataKind(HttpRequest
* req
, HttpResponse
* resp
) {
955 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
959 if (!B
.getDomainInfo(zonename
, di
)) {
960 throw HttpNotFoundException();
963 string kind
= req
->parameters
["kind"];
965 if (req
->method
== "GET") {
966 vector
<string
> metadata
;
967 Json::object document
;
970 if (!B
.getDomainMetadata(zonename
, kind
, metadata
))
971 throw HttpNotFoundException();
972 else if (!isValidMetadataKind(kind
, true))
973 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
975 document
["type"] = "Metadata";
976 document
["kind"] = kind
;
978 for (const string
& i
: metadata
)
979 entries
.push_back(i
);
981 document
["metadata"] = entries
;
982 resp
->setBody(document
);
983 } else if (req
->method
== "PUT") {
984 auto document
= req
->json();
986 if (!isValidMetadataKind(kind
, false))
987 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
989 vector
<string
> vecMetadata
;
990 auto& metadata
= document
["metadata"];
991 if (!metadata
.is_array())
992 throw ApiException("metadata is not specified or not an array");
994 for (const auto& i
: metadata
.array_items()) {
996 throw ApiException("metadata must be strings");
997 vecMetadata
.push_back(i
.string_value());
1000 if (!B
.setDomainMetadata(zonename
, kind
, vecMetadata
))
1001 throw ApiException("Could not update metadata entries for domain '" + zonename
.toString() + "'");
1004 { "type", "Metadata" },
1006 { "metadata", metadata
}
1010 } else if (req
->method
== "DELETE") {
1011 if (!isValidMetadataKind(kind
, false))
1012 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1014 vector
<string
> md
; // an empty vector will do it
1015 if (!B
.setDomainMetadata(zonename
, kind
, md
))
1016 throw ApiException("Could not delete metadata for domain '" + zonename
.toString() + "' (" + kind
+ ")");
1018 throw HttpMethodNotAllowedException();
1021 // Throws 404 if the key with inquireKeyId does not exist
1022 static void apiZoneCryptoKeysCheckKeyExists(DNSName zonename
, int inquireKeyId
, DNSSECKeeper
*dk
) {
1023 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1025 for(const auto& value
: keyset
) {
1026 if (value
.second
.id
== (unsigned) inquireKeyId
) {
1032 throw HttpNotFoundException();
1036 static void apiZoneCryptokeysGET(DNSName zonename
, int inquireKeyId
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1037 DNSSECKeeper::keyset_t keyset
=dk
->getKeys(zonename
, false);
1039 bool inquireSingleKey
= inquireKeyId
>= 0;
1042 for(const auto& value
: keyset
) {
1043 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1048 switch (value
.second
.keyType
) {
1049 case DNSSECKeeper::KSK
: keyType
="ksk"; break;
1050 case DNSSECKeeper::ZSK
: keyType
="zsk"; break;
1051 case DNSSECKeeper::CSK
: keyType
="csk"; break;
1055 { "type", "Cryptokey" },
1056 { "id", (int)value
.second
.id
},
1057 { "active", value
.second
.active
},
1058 { "keytype", keyType
},
1059 { "flags", (uint16_t)value
.first
.d_flags
},
1060 { "dnskey", value
.first
.getDNSKEY().getZoneRepresentation() },
1061 { "algorithm", DNSSECKeeper::algorithm2name(value
.first
.d_algorithm
) },
1062 { "bits", value
.first
.getKey()->getBits() }
1065 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1067 for(const uint8_t keyid
: { DNSSECKeeper::DIGEST_SHA1
, DNSSECKeeper::DIGEST_SHA256
, DNSSECKeeper::DIGEST_GOST
, DNSSECKeeper::DIGEST_SHA384
})
1069 dses
.push_back(makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation());
1074 if (inquireSingleKey
) {
1075 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1082 if (inquireSingleKey
) {
1083 // we came here because we couldn't find the requested key.
1084 throw HttpNotFoundException();
1091 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1092 * It deletes a key from :zone_name specified by :cryptokey_id.
1094 * Case 1: the backend returns true on removal. This means the key is gone.
1095 * The server returns 204 No Content, no body.
1096 * Case 2: the backend returns false on removal. An error occurred.
1097 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1098 * Case 3: the key or zone does not exist.
1099 * The server returns 404 Not Found
1101 static void apiZoneCryptokeysDELETE(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1102 if (dk
->removeKey(zonename
, inquireKeyId
)) {
1106 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1111 * This method adds a key to a zone by generate it or content parameter.
1114 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1115 * "keytype" : "ksk|zsk" <string>
1116 * "active" : "true|false" <value>
1117 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1118 * "bits" : number of bits <int>
1122 * Case 1: keytype isn't ksk|zsk
1123 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1124 * Case 2: 'bits' must be a positive integer value.
1125 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1126 * Case 3: The "algorithm" isn't supported
1127 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1128 * Case 4: Algorithm <= 10 and no bits were passed
1129 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1130 * Case 5: The wrong keysize was passed
1131 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1132 * Case 6: If the server cant guess the keysize
1133 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1134 * Case 7: The key-creation failed
1135 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1136 * Case 8: The key in content has the wrong format
1137 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1138 * Case 9: The wrong combination of fields is submitted
1139 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1140 * Case 10: No content and everything was fine
1141 * The server returns 201 Created and all public data about the new cryptokey
1142 * Case 11: With specified content
1143 * The server returns 201 Created and all public data about the added cryptokey
1146 static void apiZoneCryptokeysPOST(DNSName zonename
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1147 auto document
= req
->json();
1148 string privatekey_fieldname
= "privatekey";
1149 auto privatekey
= document
["privatekey"];
1150 if (privatekey
.is_null()) {
1151 // Fallback to the old "content" behaviour
1152 privatekey
= document
["content"];
1153 privatekey_fieldname
= "content";
1155 bool active
= boolFromJson(document
, "active", false);
1158 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1160 } else if (stringFromJson(document
, "keytype") == "zsk") {
1163 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1166 int64_t insertedId
= -1;
1168 if (privatekey
.is_null()) {
1169 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1170 auto docbits
= document
["bits"];
1171 if (!docbits
.is_null()) {
1172 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1173 throw ApiException("'bits' must be a positive integer value");
1175 bits
= docbits
.int_value();
1178 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1179 auto providedAlgo
= document
["algorithm"];
1180 if (providedAlgo
.is_string()) {
1181 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1182 if (algorithm
== -1)
1183 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1184 } else if (providedAlgo
.is_number()) {
1185 algorithm
= providedAlgo
.int_value();
1186 } else if (!providedAlgo
.is_null()) {
1187 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1191 if (!dk
->addKey(zonename
, keyOrZone
, algorithm
, insertedId
, bits
, active
)) {
1192 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1194 } catch (std::runtime_error
& error
) {
1195 throw ApiException(error
.what());
1198 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1199 } else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1200 auto keyData
= stringFromJson(document
, privatekey_fieldname
);
1201 DNSKEYRecordContent dkrc
;
1202 DNSSECPrivateKey dpk
;
1204 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1205 dpk
.d_algorithm
= dkrc
.d_algorithm
;
1206 // TODO remove in 4.2.0
1207 if(dpk
.d_algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
)
1208 dpk
.d_algorithm
= DNSSECKeeper::RSASHA1
;
1217 catch (std::runtime_error
& error
) {
1218 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1220 if (!dk
->addKey(zonename
, dpk
,insertedId
, active
)) {
1221 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1223 } catch (std::runtime_error
& error
) {
1224 throw ApiException(error
.what());
1227 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1229 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1231 apiZoneCryptokeysGET(zonename
, insertedId
, resp
, dk
);
1236 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1237 * It de/activates a key from :zone_name specified by :cryptokey_id.
1239 * Case 1: invalid JSON data
1240 * The server returns 400 Bad Request
1241 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1242 * The server returns 204 No Content
1243 * Case 3: the backend returns false on de/activation. An error occurred.
1244 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1246 static void apiZoneCryptokeysPUT(DNSName zonename
, int inquireKeyId
, HttpRequest
*req
, HttpResponse
*resp
, DNSSECKeeper
*dk
) {
1247 //throws an exception if the Body is empty
1248 auto document
= req
->json();
1249 //throws an exception if the key does not exist or is not a bool
1250 bool active
= boolFromJson(document
, "active");
1252 if (!dk
->activateKey(zonename
, inquireKeyId
)) {
1253 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1257 if (!dk
->deactivateKey(zonename
, inquireKeyId
)) {
1258 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zonename
.toString(), 422);
1268 * This method chooses the right functionality for the request. It also checks for a cryptokey_id which has to be passed
1269 * by URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1270 * If the the HTTP-request-method isn't supported, the function returns a response with the 405 code (method not allowed).
1272 static void apiZoneCryptokeys(HttpRequest
*req
, HttpResponse
*resp
) {
1273 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1276 DNSSECKeeper
dk(&B
);
1278 if (!B
.getDomainInfo(zonename
, di
)) {
1279 throw HttpNotFoundException();
1282 int inquireKeyId
= -1;
1283 if (req
->parameters
.count("key_id")) {
1284 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1285 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, &dk
);
1288 if (req
->method
== "GET") {
1289 apiZoneCryptokeysGET(zonename
, inquireKeyId
, resp
, &dk
);
1290 } else if (req
->method
== "DELETE") {
1291 if (inquireKeyId
== -1)
1292 throw HttpBadRequestException();
1293 apiZoneCryptokeysDELETE(zonename
, inquireKeyId
, req
, resp
, &dk
);
1294 } else if (req
->method
== "POST") {
1295 apiZoneCryptokeysPOST(zonename
, req
, resp
, &dk
);
1296 } else if (req
->method
== "PUT") {
1297 if (inquireKeyId
== -1)
1298 throw HttpBadRequestException();
1299 apiZoneCryptokeysPUT(zonename
, inquireKeyId
, req
, resp
, &dk
);
1301 throw HttpMethodNotAllowedException(); //Returns method not allowed
1305 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, DNSName zonename
) {
1306 DNSResourceRecord rr
;
1307 vector
<string
> zonedata
;
1308 stringtok(zonedata
, zonestring
, "\r\n");
1310 ZoneParserTNG
zpt(zonedata
, zonename
);
1311 zpt
.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
1315 string comment
= "Imported via the API";
1318 while(zpt
.get(rr
, &comment
)) {
1319 if(seenSOA
&& rr
.qtype
.getCode() == QType::SOA
)
1321 if(rr
.qtype
.getCode() == QType::SOA
)
1323 validateGatheredRRType(rr
);
1325 new_records
.push_back(rr
);
1328 catch(std::exception
& ae
) {
1329 throw ApiException("An error occurred while parsing the zonedata: "+string(ae
.what()));
1333 /** Throws ApiException if records which violate RRset contraints are present.
1334 * NOTE: sorts records in-place.
1336 * Constraints being checked:
1337 * *) no exact duplicates
1338 * *) no duplicates for QTypes that can only be present once per RRset
1339 * *) hostnames are hostnames
1341 static void checkNewRecords(vector
<DNSResourceRecord
>& records
) {
1342 sort(records
.begin(), records
.end(),
1343 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1344 /* we need _strict_ weak ordering */
1345 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1349 DNSResourceRecord previous
;
1350 for(const auto& rec
: records
) {
1351 if (previous
.qname
== rec
.qname
) {
1352 if (previous
.qtype
== rec
.qtype
) {
1353 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1354 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+" has more than one record");
1356 if (previous
.content
== rec
.content
) {
1357 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.getName() + " with content \"" + rec
.content
+ "\"");
1359 } else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1360 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName()+": Conflicts with another RRset");
1364 // Check if the DNSNames that should be hostnames, are hostnames
1366 checkHostnameCorrectness(rec
);
1367 } catch (const std::exception
& e
) {
1368 throw ApiException("RRset "+rec
.qname
.toString()+" IN "+rec
.qtype
.getName() + " " + e
.what());
1375 static void checkTSIGKey(UeberBackend
& B
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1377 string contentFromDB
;
1378 B
.getTSIGKey(keyname
, &algoFromDB
, &contentFromDB
);
1379 if (!contentFromDB
.empty() || !algoFromDB
.empty()) {
1380 throw HttpConflictException("A TSIG key with the name '"+keyname
.toLogString()+"' already exists");
1384 if (!getTSIGHashEnum(algo
, the
)) {
1385 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1389 if (B64Decode(content
, b64out
) == -1) {
1390 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1394 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
) {
1395 Json::object tsigkey
= {
1396 { "name", keyname
.toStringNoDot() },
1397 { "id", apiZoneNameToId(keyname
) },
1398 { "algorithm", algo
.toStringNoDot() },
1400 { "type", "TSIGKey" }
1405 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
=true) {
1406 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1409 static void apiServerTSIGKeys(HttpRequest
* req
, HttpResponse
* resp
) {
1411 if (req
->method
== "GET") {
1412 vector
<struct TSIGKey
> keys
;
1414 if (!B
.getTSIGKeys(keys
)) {
1415 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1420 for(const auto &key
: keys
) {
1421 doc
.push_back(makeJSONTSIGKey(key
, false));
1424 } else if (req
->method
== "POST") {
1425 auto document
= req
->json();
1426 DNSName
keyname(stringFromJson(document
, "name"));
1427 DNSName
algo(stringFromJson(document
, "algorithm"));
1428 string content
= document
["key"].string_value();
1430 if (content
.empty()) {
1432 content
= makeTSIGKey(algo
);
1433 } catch (const PDNSException
& e
) {
1434 throw HttpBadRequestException(e
.reason
);
1438 // Will throw an ApiException or HttpConflictException on error
1439 checkTSIGKey(B
, keyname
, algo
, content
);
1441 if(!B
.setTSIGKey(keyname
, algo
, content
)) {
1442 throw HttpInternalServerErrorException("Unable to add TSIG key");
1446 resp
->setBody(makeJSONTSIGKey(keyname
, algo
, content
));
1448 throw HttpMethodNotAllowedException();
1452 static void apiServerTSIGKeyDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1454 DNSName keyname
= apiZoneIdToName(req
->parameters
["id"]);
1458 if (!B
.getTSIGKey(keyname
, &algo
, &content
)) {
1459 throw HttpNotFoundException("TSIG key with name '"+keyname
.toLogString()+"' not found");
1464 tsk
.algorithm
= algo
;
1467 if (req
->method
== "GET") {
1468 resp
->setBody(makeJSONTSIGKey(tsk
));
1469 } else if (req
->method
== "PUT") {
1470 json11::Json document
;
1471 if (!req
->body
.empty()) {
1472 document
= req
->json();
1474 if (document
["name"].is_string()) {
1475 tsk
.name
= DNSName(document
["name"].string_value());
1477 if (document
["algorithm"].is_string()) {
1478 tsk
.algorithm
= DNSName(document
["algorithm"].string_value());
1481 if (!getTSIGHashEnum(tsk
.algorithm
, the
)) {
1482 throw ApiException("Unknown TSIG algorithm: " + tsk
.algorithm
.toLogString());
1485 if (document
["key"].is_string()) {
1486 string new_content
= document
["key"].string_value();
1488 if (B64Decode(new_content
, decoded
) == -1) {
1489 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1491 tsk
.key
= new_content
;
1493 if (!B
.setTSIGKey(tsk
.name
, tsk
.algorithm
, tsk
.key
)) {
1494 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1496 if (tsk
.name
!= keyname
) {
1497 // Remove the old key
1498 if (!B
.deleteTSIGKey(keyname
)) {
1499 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1502 resp
->setBody(makeJSONTSIGKey(tsk
));
1503 } else if (req
->method
== "DELETE") {
1504 if (!B
.deleteTSIGKey(keyname
)) {
1505 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + keyname
.toStringNoDot() + "'");
1511 throw HttpMethodNotAllowedException();
1515 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
) {
1517 DNSSECKeeper
dk(&B
);
1518 if (req
->method
== "POST") {
1520 auto document
= req
->json();
1521 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1522 apiCheckNameAllowedCharacters(zonename
.toString());
1523 zonename
.makeUsLowerCase();
1525 bool exists
= B
.getDomainInfo(zonename
, di
);
1527 throw HttpConflictException();
1529 // validate 'kind' is set
1530 DomainInfo::DomainKind zonekind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
1532 string zonestring
= document
["zone"].string_value();
1533 auto rrsets
= document
["rrsets"];
1534 if (rrsets
.is_array() && zonestring
!= "")
1535 throw ApiException("You cannot give rrsets AND zone data as text");
1537 auto nameservers
= document
["nameservers"];
1538 if (!nameservers
.is_null() && !nameservers
.is_array() && zonekind
!= DomainInfo::Slave
)
1539 throw ApiException("Nameservers is not a list");
1541 string soa_edit_api_kind
;
1542 if (document
["soa_edit_api"].is_string()) {
1543 soa_edit_api_kind
= document
["soa_edit_api"].string_value();
1546 soa_edit_api_kind
= "DEFAULT";
1548 string soa_edit_kind
= document
["soa_edit"].string_value();
1550 // if records/comments are given, load and check them
1551 bool have_soa
= false;
1552 bool have_zone_ns
= false;
1553 vector
<DNSResourceRecord
> new_records
;
1554 vector
<Comment
> new_comments
;
1555 vector
<DNSResourceRecord
> new_ptrs
;
1557 if (rrsets
.is_array()) {
1558 for (const auto& rrset
: rrsets
.array_items()) {
1559 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1560 apiCheckQNameAllowedCharacters(qname
.toString());
1562 qtype
= stringFromJson(rrset
, "type");
1563 if (qtype
.getCode() == 0) {
1564 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1566 if (rrset
["records"].is_array()) {
1567 int ttl
= intFromJson(rrset
, "ttl");
1568 gatherRecords(B
, req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
1570 if (rrset
["comments"].is_array()) {
1571 gatherComments(rrset
, qname
, qtype
, new_comments
);
1574 } else if (zonestring
!= "") {
1575 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1578 for(auto& rr
: new_records
) {
1579 rr
.qname
.makeUsLowerCase();
1580 if (!rr
.qname
.isPartOf(zonename
) && rr
.qname
!= zonename
)
1581 throw ApiException("RRset "+rr
.qname
.toString()+" IN "+rr
.qtype
.getName()+": Name is out of zone");
1582 apiCheckQNameAllowedCharacters(rr
.qname
.toString());
1584 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
1586 increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
1588 if (rr
.qtype
.getCode() == QType::NS
&& rr
.qname
==zonename
) {
1589 have_zone_ns
= true;
1593 // synthesize RRs as needed
1594 DNSResourceRecord autorr
;
1595 autorr
.qname
= zonename
;
1597 autorr
.ttl
= ::arg().asNum("default-ttl");
1599 if (!have_soa
&& zonekind
!= DomainInfo::Slave
) {
1600 // synthesize a SOA record so the zone "really" exists
1601 string soa
= (boost::format("%s %s %ul")
1602 % ::arg()["default-soa-name"]
1603 % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename
).toString() : ::arg()["default-soa-mail"])
1604 % document
["serial"].int_value()
1607 fillSOAData(soa
, sd
); // fills out default values for us
1608 autorr
.qtype
= QType::SOA
;
1609 autorr
.content
= makeSOAContent(sd
)->getZoneRepresentation(true);
1610 increaseSOARecord(autorr
, soa_edit_api_kind
, soa_edit_kind
);
1611 new_records
.push_back(autorr
);
1614 // create NS records if nameservers are given
1615 for (auto value
: nameservers
.array_items()) {
1616 string nameserver
= value
.string_value();
1617 if (nameserver
.empty())
1618 throw ApiException("Nameservers must be non-empty strings");
1619 if (!isCanonical(nameserver
))
1620 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1622 // ensure the name parses
1623 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1625 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1627 autorr
.qtype
= QType::NS
;
1628 new_records
.push_back(autorr
);
1630 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1634 checkNewRecords(new_records
);
1636 if (boolFromJson(document
, "dnssec", false)) {
1637 checkDefaultDNSSECAlgos();
1639 if(document
["nsec3param"].string_value().length() > 0) {
1640 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
1641 string error_msg
= "";
1642 if (!dk
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
1643 throw ApiException("NSEC3PARAMs provided for zone '"+zonename
.toString()+"' are invalid. " + error_msg
);
1648 // no going back after this
1649 if(!B
.createDomain(zonename
))
1650 throw ApiException("Creating domain '"+zonename
.toString()+"' failed");
1652 if(!B
.getDomainInfo(zonename
, di
))
1653 throw ApiException("Creating domain '"+zonename
.toString()+"' failed: lookup of domain ID failed");
1655 di
.backend
->startTransaction(zonename
, di
.id
);
1657 // updateDomainSettingsFromDocument does NOT fill out the default we've established above.
1658 if (!soa_edit_api_kind
.empty()) {
1659 di
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1662 for(auto rr
: new_records
) {
1663 rr
.domain_id
= di
.id
;
1664 di
.backend
->feedRecord(rr
, DNSName());
1666 for(Comment
& c
: new_comments
) {
1667 c
.domain_id
= di
.id
;
1668 di
.backend
->feedComment(c
);
1671 updateDomainSettingsFromDocument(B
, di
, zonename
, document
, false);
1673 di
.backend
->commitTransaction();
1675 storeChangedPTRs(B
, new_ptrs
);
1677 fillZone(B
, zonename
, resp
, shouldDoRRSets(req
));
1682 if(req
->method
!= "GET")
1683 throw HttpMethodNotAllowedException();
1685 vector
<DomainInfo
> domains
;
1687 if (req
->getvars
.count("zone")) {
1688 string zone
= req
->getvars
["zone"];
1689 apiCheckNameAllowedCharacters(zone
);
1690 DNSName zonename
= apiNameToDNSName(zone
);
1691 zonename
.makeUsLowerCase();
1693 if (B
.getDomainInfo(zonename
, di
)) {
1694 domains
.push_back(di
);
1698 B
.getAllDomains(&domains
, true); // incl. disabled
1699 } catch(const PDNSException
&e
) {
1700 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + e
.reason
);
1704 bool with_dnssec
= true;
1705 if (req
->getvars
.count("dnssec")) {
1706 // can send ?dnssec=false to improve performance.
1707 string dnssec_flag
= req
->getvars
["dnssec"];
1708 if (dnssec_flag
== "false") {
1709 with_dnssec
= false;
1714 for(const DomainInfo
& di
: domains
) {
1715 doc
.push_back(getZoneInfo(di
, with_dnssec
? &dk
: nullptr));
1720 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
) {
1721 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1726 if (!B
.getDomainInfo(zonename
, di
)) {
1727 throw HttpNotFoundException();
1729 } catch(const PDNSException
&e
) {
1730 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1733 if(req
->method
== "PUT") {
1734 // update domain settings
1736 di
.backend
->startTransaction(zonename
, -1);
1737 updateDomainSettingsFromDocument(B
, di
, zonename
, req
->json(), false);
1738 di
.backend
->commitTransaction();
1741 resp
->status
= 204; // No Content, but indicate success
1744 else if(req
->method
== "DELETE") {
1746 if(!di
.backend
->deleteDomain(zonename
))
1747 throw ApiException("Deleting domain '"+zonename
.toString()+"' failed: backend delete failed/unsupported");
1750 DNSSECKeeper::clearCaches(zonename
);
1751 purgeAuthCaches(zonename
.toString() + "$");
1753 // empty body on success
1755 resp
->status
= 204; // No Content: declare that the zone is gone now
1757 } else if (req
->method
== "PATCH") {
1758 patchZone(B
, req
, resp
);
1760 } else if (req
->method
== "GET") {
1761 fillZone(B
, zonename
, resp
, shouldDoRRSets(req
));
1764 throw HttpMethodNotAllowedException();
1767 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
) {
1768 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1770 if(req
->method
!= "GET")
1771 throw HttpMethodNotAllowedException();
1777 if (!B
.getDomainInfo(zonename
, di
)) {
1778 throw HttpNotFoundException();
1781 DNSResourceRecord rr
;
1783 di
.backend
->list(zonename
, di
.id
);
1784 while(di
.backend
->get(rr
)) {
1785 if (!rr
.qtype
.getCode())
1786 continue; // skip empty non-terminals
1789 rr
.qname
.toString() << "\t" <<
1792 rr
.qtype
.getName() << "\t" <<
1793 makeApiRecordContent(rr
.qtype
, rr
.content
) <<
1797 if (req
->accept_json
) {
1798 resp
->setBody(Json::object
{ { "zone", ss
.str() } });
1800 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
1801 resp
->body
= ss
.str();
1805 static void apiServerZoneAxfrRetrieve(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(di
.masters
.empty())
1818 throw ApiException("Domain '"+zonename
.toString()+"' is not a slave domain (or has no master defined)");
1820 random_shuffle(di
.masters
.begin(), di
.masters
.end());
1821 Communicator
.addSuckRequest(zonename
, di
.masters
.front());
1822 resp
->setSuccessResult("Added retrieval request for '"+zonename
.toString()+"' from master "+di
.masters
.front().toLogString());
1825 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
) {
1826 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1828 if(req
->method
!= "PUT")
1829 throw HttpMethodNotAllowedException();
1833 if (!B
.getDomainInfo(zonename
, di
)) {
1834 throw HttpNotFoundException();
1837 if(!Communicator
.notifyDomain(zonename
, &B
))
1838 throw ApiException("Failed to add to the queue - see server log");
1840 resp
->setSuccessResult("Notification queued");
1843 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
) {
1844 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1846 if(req
->method
!= "PUT")
1847 throw HttpMethodNotAllowedException();
1851 if (!B
.getDomainInfo(zonename
, di
)) {
1852 throw HttpNotFoundException();
1855 DNSSECKeeper
dk(&B
);
1857 if (!dk
.isSecuredZone(zonename
))
1858 throw ApiException("Zone '" + zonename
.toString() + "' is not DNSSEC signed, not rectifying.");
1860 if (di
.kind
== DomainInfo::Slave
)
1861 throw ApiException("Zone '" + zonename
.toString() + "' is a slave zone, not rectifying.");
1863 string error_msg
= "";
1865 if (!dk
.rectifyZone(zonename
, error_msg
, info
, true))
1866 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
1868 resp
->setSuccessResult("Rectified");
1871 static void makePtr(const DNSResourceRecord
& rr
, DNSResourceRecord
* ptr
) {
1872 if (rr
.qtype
.getCode() == QType::A
) {
1874 if (!IpToU32(rr
.content
, &ip
)) {
1875 throw ApiException("PTR: Invalid IP address given");
1877 ptr
->qname
= DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
1878 % ((ip
>> 24) & 0xff)
1879 % ((ip
>> 16) & 0xff)
1880 % ((ip
>> 8) & 0xff)
1883 } else if (rr
.qtype
.getCode() == QType::AAAA
) {
1884 ComboAddress
ca(rr
.content
);
1887 for (int octet
= 0; octet
< 16; ++octet
) {
1888 if (snprintf(buf
, sizeof(buf
), "%02x", ca
.sin6
.sin6_addr
.s6_addr
[octet
]) != (sizeof(buf
)-1)) {
1889 // this should be impossible: no byte should give more than two digits in hex format
1890 throw PDNSException("Formatting IPv6 address failed");
1892 ss
<< buf
[0] << '.' << buf
[1] << '.';
1894 string tmp
= ss
.str();
1895 tmp
.resize(tmp
.size()-1); // remove last dot
1896 // reverse and append arpa domain
1897 ptr
->qname
= DNSName(string(tmp
.rbegin(), tmp
.rend())) + DNSName("ip6.arpa.");
1899 throw ApiException("Unsupported PTR source '" + rr
.qname
.toString() + "' type '" + rr
.qtype
.getName() + "'");
1904 ptr
->disabled
= rr
.disabled
;
1905 ptr
->content
= rr
.qname
.toStringRootDot();
1908 static void storeChangedPTRs(UeberBackend
& B
, vector
<DNSResourceRecord
>& new_ptrs
) {
1909 for(const DNSResourceRecord
& rr
: new_ptrs
) {
1911 if (!B
.getAuth(rr
.qname
, QType(QType::PTR
), &sd
, false))
1912 throw ApiException("Could not find domain for PTR '"+rr
.qname
.toString()+"' requested for '"+rr
.content
+"' (while saving)");
1914 string soa_edit_api_kind
;
1915 string soa_edit_kind
;
1916 bool soa_changed
= false;
1917 DNSResourceRecord soarr
;
1918 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT-API", soa_edit_api_kind
);
1919 sd
.db
->getDomainMetadataOne(sd
.qname
, "SOA-EDIT", soa_edit_kind
);
1920 if (!soa_edit_api_kind
.empty()) {
1921 soa_changed
= makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, soarr
);
1924 sd
.db
->startTransaction(sd
.qname
);
1925 if (!sd
.db
->replaceRRSet(sd
.domain_id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
1926 sd
.db
->abortTransaction();
1927 throw ApiException("PTR-Hosting backend for "+rr
.qname
.toString()+"/"+rr
.qtype
.getName()+" does not support editing records.");
1931 sd
.db
->replaceRRSet(sd
.domain_id
, soarr
.qname
, soarr
.qtype
, vector
<DNSResourceRecord
>(1, soarr
));
1934 sd
.db
->commitTransaction();
1935 purgeAuthCachesExact(rr
.qname
);
1939 static void patchZone(UeberBackend
& B
, HttpRequest
* req
, HttpResponse
* resp
) {
1943 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
1944 if (!B
.getDomainInfo(zonename
, di
)) {
1945 throw HttpNotFoundException();
1948 vector
<DNSResourceRecord
> new_records
;
1949 vector
<Comment
> new_comments
;
1950 vector
<DNSResourceRecord
> new_ptrs
;
1952 Json document
= req
->json();
1954 auto rrsets
= document
["rrsets"];
1955 if (!rrsets
.is_array())
1956 throw ApiException("No rrsets given in update request");
1958 di
.backend
->startTransaction(zonename
);
1961 string soa_edit_api_kind
;
1962 string soa_edit_kind
;
1963 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
1964 di
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
1965 bool soa_edit_done
= false;
1967 set
<pair
<DNSName
, QType
>> seen
;
1969 for (const auto& rrset
: rrsets
.array_items()) {
1970 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
1971 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1972 apiCheckQNameAllowedCharacters(qname
.toString());
1974 qtype
= stringFromJson(rrset
, "type");
1975 if (qtype
.getCode() == 0) {
1976 throw ApiException("RRset "+qname
.toString()+" IN "+stringFromJson(rrset
, "type")+": unknown type given");
1979 if(seen
.count({qname
, qtype
}))
1981 throw ApiException("Duplicate RRset "+qname
.toString()+" IN "+qtype
.getName());
1983 seen
.insert({qname
, qtype
});
1985 if (changetype
== "DELETE") {
1986 // delete all matching qname/qtype RRs (and, implicitly comments).
1987 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
1988 throw ApiException("Hosting backend does not support editing records.");
1991 else if (changetype
== "REPLACE") {
1992 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
1993 if (!qname
.isPartOf(zonename
) && qname
!= zonename
)
1994 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Name is out of zone");
1996 bool replace_records
= rrset
["records"].is_array();
1997 bool replace_comments
= rrset
["comments"].is_array();
1999 if (!replace_records
&& !replace_comments
) {
2000 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.getName());
2003 new_records
.clear();
2004 new_comments
.clear();
2006 if (replace_records
) {
2007 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2008 int ttl
= intFromJson(rrset
, "ttl");
2009 // new_ptrs is merged.
2010 gatherRecords(B
, req
->logprefix
, rrset
, qname
, qtype
, ttl
, new_records
, new_ptrs
);
2012 for(DNSResourceRecord
& rr
: new_records
) {
2013 rr
.domain_id
= di
.id
;
2014 if (rr
.qtype
.getCode() == QType::SOA
&& rr
.qname
==zonename
) {
2015 soa_edit_done
= increaseSOARecord(rr
, soa_edit_api_kind
, soa_edit_kind
);
2018 checkNewRecords(new_records
);
2021 if (replace_comments
) {
2022 gatherComments(rrset
, qname
, qtype
, new_comments
);
2024 for(Comment
& c
: new_comments
) {
2025 c
.domain_id
= di
.id
;
2029 if (replace_records
) {
2030 bool ent_present
= false;
2031 di
.backend
->lookup(QType(QType::ANY
), qname
, di
.id
);
2032 DNSResourceRecord rr
;
2033 while (di
.backend
->get(rr
)) {
2034 if (rr
.qtype
.getCode() == QType::ENT
) {
2036 /* that's fine, we will override it */
2039 if (qtype
.getCode() != rr
.qtype
.getCode()
2040 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2041 || exclusiveEntryTypes
.count(rr
.qtype
.getCode()) != 0)) {
2043 // leave database handle in a consistent state
2044 while (di
.backend
->get(rr
))
2047 throw ApiException("RRset "+qname
.toString()+" IN "+qtype
.getName()+": Conflicts with pre-existing RRset");
2051 if (!new_records
.empty() && ent_present
) {
2053 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qt_ent
, new_records
)) {
2054 throw ApiException("Hosting backend does not support editing records.");
2057 if (!di
.backend
->replaceRRSet(di
.id
, qname
, qtype
, new_records
)) {
2058 throw ApiException("Hosting backend does not support editing records.");
2061 if (replace_comments
) {
2062 if (!di
.backend
->replaceComments(di
.id
, qname
, qtype
, new_comments
)) {
2063 throw ApiException("Hosting backend does not support editing comments.");
2068 throw ApiException("Changetype not understood");
2071 zone_disabled
= (!B
.getSOAUncached(zonename
, sd
));
2073 // edit SOA (if needed)
2074 if (!zone_disabled
&& !soa_edit_api_kind
.empty() && !soa_edit_done
) {
2075 DNSResourceRecord rr
;
2076 if (makeIncreasedSOARecord(sd
, soa_edit_api_kind
, soa_edit_kind
, rr
)) {
2077 if (!di
.backend
->replaceRRSet(di
.id
, rr
.qname
, rr
.qtype
, vector
<DNSResourceRecord
>(1, rr
))) {
2078 throw ApiException("Hosting backend does not support editing records.");
2082 // return old and new serials in headers
2083 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(sd
.serial
);
2084 fillSOAData(rr
.content
, sd
);
2085 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(sd
.serial
);
2089 di
.backend
->abortTransaction();
2094 DNSSECKeeper
dk(&B
);
2095 if (!zone_disabled
&& !dk
.isPresigned(zonename
)) {
2097 if (!di
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
) && ::arg().mustDo("default-api-rectify")) {
2100 if (api_rectify
== "1") {
2103 if (!dk
.rectifyZone(zonename
, error_msg
, info
, false)) {
2104 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2109 di
.backend
->commitTransaction();
2111 purgeAuthCaches(zonename
.toString() + "$");
2114 storeChangedPTRs(B
, new_ptrs
);
2117 resp
->status
= 204; // No Content, but indicate success
2121 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
2122 if(req
->method
!= "GET")
2123 throw HttpMethodNotAllowedException();
2125 string q
= req
->getvars
["q"];
2126 string sMax
= req
->getvars
["max"];
2127 string sObjectType
= req
->getvars
["object_type"];
2132 // the following types of data can be searched for using the api
2133 enum class ObjectType
2142 throw ApiException("Query q can't be blank");
2144 maxEnts
= std::stoi(sMax
);
2146 throw ApiException("Maximum entries must be larger than 0");
2148 if (sObjectType
.empty())
2149 objectType
= ObjectType::ALL
;
2150 else if (sObjectType
== "all")
2151 objectType
= ObjectType::ALL
;
2152 else if (sObjectType
== "zone")
2153 objectType
= ObjectType::ZONE
;
2154 else if (sObjectType
== "record")
2155 objectType
= ObjectType::RECORD
;
2156 else if (sObjectType
== "comment")
2157 objectType
= ObjectType::COMMENT
;
2159 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2161 SimpleMatch
sm(q
,true);
2163 vector
<DomainInfo
> domains
;
2164 vector
<DNSResourceRecord
> result_rr
;
2165 vector
<Comment
> result_c
;
2166 map
<int,DomainInfo
> zoneIdZone
;
2167 map
<int,DomainInfo
>::iterator val
;
2170 B
.getAllDomains(&domains
, true);
2172 for(const DomainInfo di
: domains
)
2174 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& sm
.match(di
.zone
)) {
2175 doc
.push_back(Json::object
{
2176 { "object_type", "zone" },
2177 { "zone_id", apiZoneNameToId(di
.zone
) },
2178 { "name", di
.zone
.toString() }
2182 zoneIdZone
[di
.id
] = di
; // populate cache
2185 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && B
.searchRecords(q
, maxEnts
, result_rr
))
2187 for(const DNSResourceRecord
& rr
: result_rr
)
2189 if (!rr
.qtype
.getCode())
2190 continue; // skip empty non-terminals
2192 auto object
= Json::object
{
2193 { "object_type", "record" },
2194 { "name", rr
.qname
.toString() },
2195 { "type", rr
.qtype
.getName() },
2196 { "ttl", (double)rr
.ttl
},
2197 { "disabled", rr
.disabled
},
2198 { "content", makeApiRecordContent(rr
.qtype
, rr
.content
) }
2200 if ((val
= zoneIdZone
.find(rr
.domain_id
)) != zoneIdZone
.end()) {
2201 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2202 object
["zone"] = val
->second
.zone
.toString();
2204 doc
.push_back(object
);
2208 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && B
.searchComments(q
, maxEnts
, result_c
))
2210 for(const Comment
&c
: result_c
)
2212 auto object
= Json::object
{
2213 { "object_type", "comment" },
2214 { "name", c
.qname
.toString() },
2215 { "content", c
.content
}
2217 if ((val
= zoneIdZone
.find(c
.domain_id
)) != zoneIdZone
.end()) {
2218 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2219 object
["zone"] = val
->second
.zone
.toString();
2221 doc
.push_back(object
);
2228 void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
2229 if(req
->method
!= "PUT")
2230 throw HttpMethodNotAllowedException();
2232 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2234 uint64_t count
= purgeAuthCachesExact(canon
);
2235 resp
->setBody(Json::object
{
2236 { "count", (int) count
},
2237 { "result", "Flushed cache." }
2241 void AuthWebServer::cssfunction(HttpRequest
* req
, HttpResponse
* resp
)
2243 resp
->headers
["Cache-Control"] = "max-age=86400";
2244 resp
->headers
["Content-Type"] = "text/css";
2247 ret
<<"* { box-sizing: border-box; margin: 0; padding: 0; }"<<endl
;
2248 ret
<<"body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }"<<endl
;
2249 ret
<<"a { color: #0959c2; }"<<endl
;
2250 ret
<<"a:hover { color: #3B8EC8; }"<<endl
;
2251 ret
<<".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }"<<endl
;
2252 ret
<<".row:before, .row:after { display: table; content:\" \"; }"<<endl
;
2253 ret
<<".row:after { clear: both; }"<<endl
;
2254 ret
<<".columns { position: relative; min-height: 1px; float: left; }"<<endl
;
2255 ret
<<".all { width: 100%; }"<<endl
;
2256 ret
<<".headl { width: 60%; }"<<endl
;
2257 ret
<<".headr { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2258 ret
<<"background-image: url(data:image/png;base64,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);";
2259 ret
<<" width: 154px; height: 20px; }"<<endl
;
2260 ret
<<"a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }"<<endl
;
2261 ret
<<"footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }"<<endl
;
2262 ret
<<"footer.row { margin-top: 1em; margin-bottom: 1em; }"<<endl
;
2263 ret
<<".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }"<<endl
;
2264 ret
<<"table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }"<<endl
;
2265 ret
<<"table.data td { border-bottom: 1px solid #333; padding: 2px; }"<<endl
;
2266 ret
<<"table.data tr:nth-child(2n) { background: #e2e2e2; }"<<endl
;
2267 ret
<<"table.data tr:hover { background: white; }"<<endl
;
2268 ret
<<".ringmeta { margin-bottom: 5px; }"<<endl
;
2269 ret
<<".resetring {float: right; }"<<endl
;
2270 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
;
2271 ret
<<".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}"<<endl
;
2272 ret
<<".resizering {float: right;}"<<endl
;
2273 resp
->body
= ret
.str();
2277 void AuthWebServer::webThread()
2280 setThreadName("pdns/webserver");
2281 if(::arg().mustDo("api")) {
2282 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
2283 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
2284 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
2285 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
2286 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", &apiServerTSIGKeyDetail
);
2287 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", &apiServerTSIGKeys
);
2288 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve
);
2289 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys
);
2290 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys
);
2291 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", &apiServerZoneExport
);
2292 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", &apiZoneMetadataKind
);
2293 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", &apiZoneMetadata
);
2294 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", &apiServerZoneNotify
);
2295 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", &apiServerZoneRectify
);
2296 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
2297 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
2298 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
2299 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
2300 d_ws
->registerApiHandler("/api", &apiDiscovery
);
2302 if (::arg().mustDo("webserver")) {
2303 d_ws
->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction
, this, _1
, _2
));
2304 d_ws
->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction
, this, _1
, _2
));
2309 g_log
<<Logger::Error
<<"AuthWebServer thread caught an exception, dying"<<endl
;