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.
22 #include "dnsbackend.hh"
23 #include "webserver.hh"
29 #include "dynlistener.hh"
36 #include "arguments.hh"
39 #include "ueberbackend.hh"
40 #include <boost/format.hpp>
42 #include "namespaces.hh"
45 #include "dnsseckeeper.hh"
47 #include "zoneparser-tng.hh"
48 #include "auth-main.hh"
49 #include "auth-caches.hh"
50 #include "auth-zonecache.hh"
51 #include "threadname.hh"
52 #include "tsigutils.hh"
56 Ewma::Ewma() { dt
.set(); }
58 void Ewma::submit(int val
)
60 int rate
= val
- d_last
;
61 double difft
= dt
.udiff() / 1000000.0;
64 d_10
= ((600.0 - difft
) * d_10
+ (difft
* rate
)) / 600.0;
65 d_5
= ((300.0 - difft
) * d_5
+ (difft
* rate
)) / 300.0;
66 d_1
= ((60.0 - difft
) * d_1
+ (difft
* rate
)) / 60.0;
67 d_max
= max(d_1
, d_max
);
72 double Ewma::get10() const
77 double Ewma::get5() const
82 double Ewma::get1() const
87 double Ewma::getMax() const
92 static void patchZone(UeberBackend
& backend
, const DNSName
& zonename
, DomainInfo
& domainInfo
, HttpRequest
* req
, HttpResponse
* resp
);
94 // QTypes that MUST NOT have multiple records of the same type in a given RRset.
95 static const std::set
<uint16_t> onlyOneEntryTypes
= {QType::CNAME
, QType::DNAME
, QType::SOA
};
96 // QTypes that MUST NOT be used with any other QType on the same name.
97 static const std::set
<uint16_t> exclusiveEntryTypes
= {QType::CNAME
};
98 // QTypes that MUST be at apex.
99 static const std::set
<uint16_t> atApexTypes
= {QType::SOA
, QType::DNSKEY
};
100 // QTypes that are NOT allowed at apex.
101 static const std::set
<uint16_t> nonApexTypes
= {QType::DS
};
103 AuthWebServer::AuthWebServer() :
104 d_start(time(nullptr)),
109 if (arg().mustDo("webserver") || arg().mustDo("api")) {
110 d_ws
= std::make_unique
<WebServer
>(arg()["webserver-address"], arg().asNum("webserver-port"));
111 d_ws
->setApiKey(arg()["api-key"], arg().mustDo("webserver-hash-plaintext-credentials"));
112 d_ws
->setPassword(arg()["webserver-password"], arg().mustDo("webserver-hash-plaintext-credentials"));
113 d_ws
->setLogLevel(arg()["webserver-loglevel"]);
116 acl
.toMasks(::arg()["webserver-allow-from"]);
119 d_ws
->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
125 void AuthWebServer::go(StatBag
& stats
)
128 std::thread
webT([this]() { webThread(); });
130 std::thread
statT([this, &stats
]() { statThread(stats
); });
134 void AuthWebServer::statThread(StatBag
& stats
)
137 setThreadName("pdns/statHelper");
139 d_queries
.submit(static_cast<int>(stats
.read("udp-queries")));
140 d_cachehits
.submit(static_cast<int>(stats
.read("packetcache-hit")));
141 d_cachemisses
.submit(static_cast<int>(stats
.read("packetcache-miss")));
142 d_qcachehits
.submit(static_cast<int>(stats
.read("query-cache-hit")));
143 d_qcachemisses
.submit(static_cast<int>(stats
.read("query-cache-miss")));
148 g_log
<< Logger::Error
<< "Webserver statThread caught an exception, dying" << endl
;
153 static string
htmlescape(const string
& inputString
)
156 for (char currentChar
: inputString
) {
157 switch (currentChar
) {
171 result
+= currentChar
;
177 static void printtable(ostringstream
& ret
, const string
& ringname
, const string
& title
, int limit
= 10)
179 unsigned int tot
= 0;
181 vector
<pair
<string
, unsigned int>> ring
= S
.getRing(ringname
);
183 for (const auto& entry
: ring
) {
188 ret
<< "<div class=\"panel\">";
189 ret
<< "<span class=resetring><i></i><a href=\"?resetring=" << htmlescape(ringname
) << "\">Reset</a></span>" << endl
;
190 ret
<< "<h2>" << title
<< "</h2>" << endl
;
191 ret
<< "<div class=ringmeta>";
192 ret
<< "<a class=topXofY href=\"?ring=" << htmlescape(ringname
) << "\">Showing: Top " << limit
<< " of " << entries
<< "</a>" << endl
;
193 ret
<< "<span class=resizering>Resize: ";
194 std::vector
<uint64_t> sizes
{10, 100, 500, 1000, 10000, 500000, 0};
195 for (int i
= 0; sizes
[i
] != 0; ++i
) {
196 if (S
.getRingSize(ringname
) != sizes
[i
]) {
197 ret
<< "<a href=\"?resizering=" << htmlescape(ringname
) << "&size=" << sizes
[i
] << "\">" << sizes
[i
] << "</a> ";
200 ret
<< "(" << sizes
[i
] << ") ";
203 ret
<< "</span></div>";
205 ret
<< "<table class=\"data\">";
206 unsigned int printed
= 0;
207 unsigned int total
= std::max(1U, tot
);
208 for (auto i
= ring
.begin(); limit
!= 0 && i
!= ring
.end(); ++i
, --limit
) {
209 ret
<< "<tr><td>" << htmlescape(i
->first
) << "</td><td>" << i
->second
<< "</td><td align=right>" << AuthWebServer::makePercentage(i
->second
* 100.0 / total
) << "</td>" << endl
;
210 printed
+= i
->second
;
212 ret
<< "<tr><td colspan=3></td></tr>" << endl
;
213 if (printed
!= tot
) {
214 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
;
217 ret
<< "<tr><td><b>Total:</b></td><td><b>" << tot
<< "</b></td><td align=right><b>100%</b></td>";
218 ret
<< "</table></div>" << endl
;
221 static void printvars(ostringstream
& ret
)
223 ret
<< "<div class=panel><h2>Variables</h2><table class=\"data\">" << endl
;
225 vector
<string
> entries
= S
.getEntries();
226 for (const auto& entry
: entries
) {
227 ret
<< "<tr><td>" << entry
<< "</td><td>" << S
.read(entry
) << "</td><td>" << S
.getDescrip(entry
) << "</td>" << endl
;
230 ret
<< "</table></div>" << endl
;
233 static void printargs(ostringstream
& ret
)
235 ret
<< R
"(<table border=1><tr><td colspan=3 bgcolor="#0000ff"><font color="#ffffff">Arguments</font></td>)" << endl;
237 vector
<string
> entries
= arg().list();
238 for (const auto& entry
: entries
) {
239 ret
<< "<tr><td>" << entry
<< "</td><td>" << arg()[entry
] << "</td><td>" << arg().getHelp(entry
) << "</td>" << endl
;
243 string
AuthWebServer::makePercentage(const double& val
)
245 return (boost::format("%.01f%%") % val
).str();
248 void AuthWebServer::indexfunction(HttpRequest
* req
, HttpResponse
* resp
)
250 if (!req
->getvars
["resetring"].empty()) {
251 if (S
.ringExists(req
->getvars
["resetring"])) {
252 S
.resetRing(req
->getvars
["resetring"]);
255 resp
->headers
["Location"] = req
->url
.path
;
258 if (!req
->getvars
["resizering"].empty()) {
259 int size
= std::stoi(req
->getvars
["size"]);
260 if (S
.ringExists(req
->getvars
["resizering"]) && size
> 0 && size
<= 500000) {
261 S
.resizeRing(req
->getvars
["resizering"], std::stoi(req
->getvars
["size"]));
264 resp
->headers
["Location"] = req
->url
.path
;
270 ret
<< "<!DOCTYPE html>" << endl
;
271 ret
<< "<html><head>" << endl
;
272 ret
<< "<title>PowerDNS Authoritative Server Monitor</title>" << endl
;
273 ret
<< R
"(<link rel="stylesheet
" href="style
.css
"/>)" << endl
;
274 ret
<< "</head><body>" << endl
;
276 ret
<< "<div class=\"row\">" << endl
;
277 ret
<< "<div class=\"headl columns\">";
278 ret
<< R
"(<a href="/" id="appname
">PowerDNS )" << htmlescape(VERSION
);
279 if (!arg()["config-name"].empty()) {
280 ret
<< " [" << htmlescape(arg()["config-name"]) << "]";
282 ret
<< "</a></div>" << endl
;
283 ret
<< "<div class=\"header columns\"></div></div>";
284 ret
<< R
"(<div class="row
"><div class="all columns
">)";
286 time_t passed
= time(nullptr) - g_starttime
;
288 ret
<< "<p>Uptime: " << humanDuration(passed
) << "<br>" << endl
;
290 ret
<< "Queries/second, 1, 5, 10 minute averages: " << std::setprecision(3) << (int)d_queries
.get1() << ", " << (int)d_queries
.get5() << ", " << (int)d_queries
.get10() << ". Max queries/second: " << (int)d_queries
.getMax() << "<br>" << endl
;
292 if (d_cachemisses
.get10() + d_cachehits
.get10() > 0) {
293 ret
<< "Cache hitrate, 1, 5, 10 minute averages: " << makePercentage((d_cachehits
.get1() * 100.0) / ((d_cachehits
.get1()) + (d_cachemisses
.get1()))) << ", " << makePercentage((d_cachehits
.get5() * 100.0) / ((d_cachehits
.get5()) + (d_cachemisses
.get5()))) << ", " << makePercentage((d_cachehits
.get10() * 100.0) / ((d_cachehits
.get10()) + (d_cachemisses
.get10()))) << "<br>" << endl
;
296 if (d_qcachemisses
.get10() + d_qcachehits
.get10() > 0) {
297 ret
<< "Backend query cache hitrate, 1, 5, 10 minute averages: " << std::setprecision(2) << makePercentage((d_qcachehits
.get1() * 100.0) / ((d_qcachehits
.get1()) + (d_qcachemisses
.get1()))) << ", " << makePercentage((d_qcachehits
.get5() * 100.0) / ((d_qcachehits
.get5()) + (d_qcachemisses
.get5()))) << ", " << makePercentage((d_qcachehits
.get10() * 100.0) / ((d_qcachehits
.get10()) + (d_qcachemisses
.get10()))) << "<br>" << endl
;
300 ret
<< "Backend query load, 1, 5, 10 minute averages: " << std::setprecision(3) << (int)d_qcachemisses
.get1() << ", " << (int)d_qcachemisses
.get5() << ", " << (int)d_qcachemisses
.get10() << ". Max queries/second: " << (int)d_qcachemisses
.getMax() << "<br>" << endl
;
302 ret
<< "Total queries: " << S
.read("udp-queries") << ". Question/answer latency: " << static_cast<double>(S
.read("latency")) / 1000.0 << "ms</p><br>" << endl
;
303 if (req
->getvars
["ring"].empty()) {
304 auto entries
= S
.listRings();
305 for (const auto& entry
: entries
) {
306 printtable(ret
, entry
, S
.getRingTitle(entry
));
310 if (arg().mustDo("webserver-print-arguments")) {
314 else if (S
.ringExists(req
->getvars
["ring"])) {
315 printtable(ret
, req
->getvars
["ring"], S
.getRingTitle(req
->getvars
["ring"]), 100);
318 ret
<< "</div></div>" << endl
;
319 ret
<< "<footer class=\"row\">" << fullVersionString() << "<br>© <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>" << endl
;
320 ret
<< "</body></html>" << endl
;
322 resp
->body
= ret
.str();
326 /** Helper to build a record content as needed. */
327 static inline string
makeRecordContent(const QType
& qtype
, const string
& content
, bool noDot
)
329 // noDot: for backend storage, pass true. for API users, pass false.
330 auto drc
= DNSRecordContent::make(qtype
.getCode(), QClass::IN
, content
);
331 return drc
->getZoneRepresentation(noDot
);
334 /** "Normalize" record content for API consumers. */
335 static inline string
makeApiRecordContent(const QType
& qtype
, const string
& content
)
337 return makeRecordContent(qtype
, content
, false);
340 /** "Normalize" record content for backend storage. */
341 static inline string
makeBackendRecordContent(const QType
& qtype
, const string
& content
)
343 return makeRecordContent(qtype
, content
, true);
346 static Json::object
getZoneInfo(const DomainInfo
& domainInfo
, DNSSECKeeper
* dnssecKeeper
)
348 string zoneId
= apiZoneNameToId(domainInfo
.zone
);
349 vector
<string
> primaries
;
350 primaries
.reserve(domainInfo
.primaries
.size());
351 for (const auto& primary
: domainInfo
.primaries
) {
352 primaries
.push_back(primary
.toStringWithPortExcept(53));
355 auto obj
= Json::object
{
356 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
358 {"url", "/api/v1/servers/localhost/zones/" + zoneId
},
359 {"name", domainInfo
.zone
.toString()},
360 {"kind", domainInfo
.getKindString()},
361 {"catalog", (!domainInfo
.catalog
.empty() ? domainInfo
.catalog
.toString() : "")},
362 {"account", domainInfo
.account
},
363 {"masters", std::move(primaries
)},
364 {"serial", (double)domainInfo
.serial
},
365 {"notified_serial", (double)domainInfo
.notified_serial
},
366 {"last_check", (double)domainInfo
.last_check
}};
367 if (dnssecKeeper
!= nullptr) {
368 obj
["dnssec"] = dnssecKeeper
->isSecuredZone(domainInfo
.zone
);
370 dnssecKeeper
->getSoaEdit(domainInfo
.zone
, soa_edit
, false);
371 obj
["edited_serial"] = (double)calculateEditSOA(domainInfo
.serial
, soa_edit
, domainInfo
.zone
);
376 static bool shouldDoRRSets(HttpRequest
* req
)
378 if (req
->getvars
.count("rrsets") == 0 || req
->getvars
["rrsets"] == "true") {
381 if (req
->getvars
["rrsets"] == "false") {
385 throw ApiException("'rrsets' request parameter value '" + req
->getvars
["rrsets"] + "' is not supported");
388 static void fillZone(UeberBackend
& backend
, const DNSName
& zonename
, HttpResponse
* resp
, HttpRequest
* req
)
390 DomainInfo domainInfo
;
392 if (!backend
.getDomainInfo(zonename
, domainInfo
)) {
393 throw HttpNotFoundException();
396 DNSSECKeeper
dnssecKeeper(&backend
);
397 Json::object doc
= getZoneInfo(domainInfo
, &dnssecKeeper
);
398 // extra stuff getZoneInfo doesn't do for us (more expensive)
400 domainInfo
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api
);
401 doc
["soa_edit_api"] = soa_edit_api
;
403 domainInfo
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit
);
404 doc
["soa_edit"] = soa_edit
;
407 bool nsec3narrowbool
= false;
408 bool is_secured
= dnssecKeeper
.isSecuredZone(zonename
);
409 if (is_secured
) { // ignore NSEC3PARAM and NSEC3NARROW metadata present in the db for unsigned zones
410 domainInfo
.backend
->getDomainMetadataOne(zonename
, "NSEC3PARAM", nsec3param
);
412 domainInfo
.backend
->getDomainMetadataOne(zonename
, "NSEC3NARROW", nsec3narrow
);
413 if (nsec3narrow
== "1") {
414 nsec3narrowbool
= true;
417 doc
["nsec3param"] = nsec3param
;
418 doc
["nsec3narrow"] = nsec3narrowbool
;
419 doc
["dnssec"] = is_secured
;
422 domainInfo
.backend
->getDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
);
423 doc
["api_rectify"] = (api_rectify
== "1");
426 vector
<string
> tsig_primary
;
427 vector
<string
> tsig_secondary
;
428 domainInfo
.backend
->getDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", tsig_primary
);
429 domainInfo
.backend
->getDomainMetadata(zonename
, "AXFR-MASTER-TSIG", tsig_secondary
);
431 Json::array tsig_primary_keys
;
432 for (const auto& keyname
: tsig_primary
) {
433 tsig_primary_keys
.emplace_back(apiZoneNameToId(DNSName(keyname
)));
435 doc
["master_tsig_key_ids"] = tsig_primary_keys
;
437 Json::array tsig_secondary_keys
;
438 for (const auto& keyname
: tsig_secondary
) {
439 tsig_secondary_keys
.emplace_back(apiZoneNameToId(DNSName(keyname
)));
441 doc
["slave_tsig_key_ids"] = tsig_secondary_keys
;
443 if (shouldDoRRSets(req
)) {
444 vector
<DNSResourceRecord
> records
;
445 vector
<Comment
> comments
;
447 // load all records + sort
449 DNSResourceRecord resourceRecord
;
450 if (req
->getvars
.count("rrset_name") == 0) {
451 domainInfo
.backend
->list(zonename
, static_cast<int>(domainInfo
.id
), true); // incl. disabled
455 if (req
->getvars
.count("rrset_type") == 0) {
459 qType
= req
->getvars
["rrset_type"];
461 domainInfo
.backend
->lookup(qType
, DNSName(req
->getvars
["rrset_name"]), static_cast<int>(domainInfo
.id
));
463 while (domainInfo
.backend
->get(resourceRecord
)) {
464 if (resourceRecord
.qtype
.getCode() == 0) {
465 continue; // skip empty non-terminals
467 records
.push_back(resourceRecord
);
469 sort(records
.begin(), records
.end(), [](const DNSResourceRecord
& rrA
, const DNSResourceRecord
& rrB
) {
470 /* if you ever want to update this comparison function,
471 please be aware that you will also need to update the conditions in the code merging
472 the records and comments below */
473 if (rrA
.qname
== rrB
.qname
) {
474 return rrB
.qtype
< rrA
.qtype
;
476 return rrB
.qname
< rrA
.qname
;
480 // load all comments + sort
483 domainInfo
.backend
->listComments(domainInfo
.id
);
484 while (domainInfo
.backend
->getComment(comment
)) {
485 comments
.push_back(comment
);
487 sort(comments
.begin(), comments
.end(), [](const Comment
& rrA
, const Comment
& rrB
) {
488 /* if you ever want to update this comparison function,
489 please be aware that you will also need to update the conditions in the code merging
490 the records and comments below */
491 if (rrA
.qname
== rrB
.qname
) {
492 return rrB
.qtype
< rrA
.qtype
;
494 return rrB
.qname
< rrA
.qname
;
500 Json::array rrset_records
;
501 Json::array rrset_comments
;
502 DNSName current_qname
;
505 auto rit
= records
.begin();
506 auto cit
= comments
.begin();
508 while (rit
!= records
.end() || cit
!= comments
.end()) {
509 // 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
510 if (cit
== comments
.end() || (rit
!= records
.end() && (rit
->qname
== cit
->qname
? (cit
->qtype
< rit
->qtype
|| cit
->qtype
== rit
->qtype
) : cit
->qname
< rit
->qname
))) {
511 current_qname
= rit
->qname
;
512 current_qtype
= rit
->qtype
;
516 current_qname
= cit
->qname
;
517 current_qtype
= cit
->qtype
;
521 while (rit
!= records
.end() && rit
->qname
== current_qname
&& rit
->qtype
== current_qtype
) {
522 ttl
= min(ttl
, rit
->ttl
);
523 rrset_records
.push_back(Json::object
{
524 {"disabled", rit
->disabled
},
525 {"content", makeApiRecordContent(rit
->qtype
, rit
->content
)}});
528 while (cit
!= comments
.end() && cit
->qname
== current_qname
&& cit
->qtype
== current_qtype
) {
529 rrset_comments
.push_back(Json::object
{
530 {"modified_at", (double)cit
->modified_at
},
531 {"account", cit
->account
},
532 {"content", cit
->content
}});
536 rrset
["name"] = current_qname
.toString();
537 rrset
["type"] = current_qtype
.toString();
538 rrset
["records"] = rrset_records
;
539 rrset
["comments"] = rrset_comments
;
540 rrset
["ttl"] = (double)ttl
;
541 rrsets
.emplace_back(rrset
);
543 rrset_records
.clear();
544 rrset_comments
.clear();
547 doc
["rrsets"] = rrsets
;
550 resp
->setJsonBody(doc
);
553 void productServerStatisticsFetch(map
<string
, string
>& out
)
555 vector
<string
> items
= S
.getEntries();
556 for (const string
& item
: items
) {
557 out
[item
] = std::to_string(S
.read(item
));
561 out
["uptime"] = std::to_string(time(nullptr) - g_starttime
);
564 std::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
567 // ::read() calls ::exists() which throws a PDNSException when the key does not exist
575 static void validateGatheredRRType(const DNSResourceRecord
& resourceRecord
)
577 if (resourceRecord
.qtype
.getCode() == QType::OPT
|| resourceRecord
.qtype
.getCode() == QType::TSIG
) {
578 throw ApiException("RRset " + resourceRecord
.qname
.toString() + " IN " + resourceRecord
.qtype
.toString() + ": invalid type given");
582 static void gatherRecords(const Json
& container
, const DNSName
& qname
, const QType
& qtype
, const uint32_t ttl
, vector
<DNSResourceRecord
>& new_records
)
584 DNSResourceRecord resourceRecord
;
585 resourceRecord
.qname
= qname
;
586 resourceRecord
.qtype
= qtype
;
587 resourceRecord
.auth
= true;
588 resourceRecord
.ttl
= ttl
;
590 validateGatheredRRType(resourceRecord
);
591 const auto& items
= container
["records"].array_items();
592 for (const auto& record
: items
) {
593 string content
= stringFromJson(record
, "content");
594 if (record
.object_items().count("priority") > 0) {
595 throw std::runtime_error("`priority` element is not allowed in record");
597 resourceRecord
.disabled
= false;
598 if (!record
["disabled"].is_null()) {
599 resourceRecord
.disabled
= boolFromJson(record
, "disabled");
602 // validate that the client sent something we can actually parse, and require that data to be dotted.
604 if (resourceRecord
.qtype
.getCode() != QType::AAAA
) {
605 string tmp
= makeApiRecordContent(resourceRecord
.qtype
, content
);
606 if (!pdns_iequals(tmp
, content
)) {
607 throw std::runtime_error("Not in expected format (parsed as '" + tmp
+ "')");
611 struct in6_addr tmpbuf
614 if (inet_pton(AF_INET6
, content
.c_str(), &tmpbuf
) != 1 || content
.find('.') != string::npos
) {
615 throw std::runtime_error("Invalid IPv6 address");
618 resourceRecord
.content
= makeBackendRecordContent(resourceRecord
.qtype
, content
);
620 catch (std::exception
& e
) {
621 throw ApiException("Record " + resourceRecord
.qname
.toString() + "/" + resourceRecord
.qtype
.toString() + " '" + content
+ "': " + e
.what());
624 new_records
.push_back(resourceRecord
);
628 static void gatherComments(const Json
& container
, const DNSName
& qname
, const QType
& qtype
, vector
<Comment
>& new_comments
)
631 comment
.qname
= qname
;
632 comment
.qtype
= qtype
;
634 time_t now
= time(nullptr);
635 for (const auto& currentComment
: container
["comments"].array_items()) {
636 // FIXME 2036 issue internally in uintFromJson
637 comment
.modified_at
= uintFromJson(currentComment
, "modified_at", now
);
638 comment
.content
= stringFromJson(currentComment
, "content");
639 comment
.account
= stringFromJson(currentComment
, "account");
640 new_comments
.push_back(comment
);
644 static void checkDefaultDNSSECAlgos()
646 int k_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
647 int z_algo
= DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
648 int k_size
= arg().asNum("default-ksk-size");
649 int z_size
= arg().asNum("default-zsk-size");
651 // Sanity check DNSSEC parameters
652 if (!::arg()["default-zsk-algorithm"].empty()) {
654 throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
656 if (k_algo
<= 10 && k_size
== 0) {
657 throw ApiException("default-ksk-algorithm is set to an algorithm(" + ::arg()["default-ksk-algorithm"] + ") that requires a non-zero default-ksk-size!");
661 if (!::arg()["default-zsk-algorithm"].empty()) {
663 throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
665 if (z_algo
<= 10 && z_size
== 0) {
666 throw ApiException("default-zsk-algorithm is set to an algorithm(" + ::arg()["default-zsk-algorithm"] + ") that requires a non-zero default-zsk-size!");
671 static void throwUnableToSecure(const DNSName
& zonename
)
673 throw ApiException("No backend was able to secure '" + zonename
.toString() + "', most likely because no DNSSEC"
674 + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
678 * Add KSK and ZSK to an existing zone. Algorithms and sizes will be chosen per configuration.
680 static void addDefaultDNSSECKeys(DNSSECKeeper
& dnssecKeeper
, const DNSName
& zonename
)
682 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 (!dnssecKeeper
.addKey(zonename
, true, k_algo
, keyID
, k_size
)) {
691 throwUnableToSecure(zonename
);
697 if (!dnssecKeeper
.addKey(zonename
, false, z_algo
, keyID
, z_size
)) {
698 throwUnableToSecure(zonename
);
703 static bool isZoneApiRectifyEnabled(const DomainInfo
& domainInfo
)
706 domainInfo
.backend
->getDomainMetadataOne(domainInfo
.zone
, "API-RECTIFY", api_rectify
);
707 if (api_rectify
.empty() && ::arg().mustDo("default-api-rectify")) {
710 return api_rectify
== "1";
713 static void extractDomainInfoFromDocument(const Json
& document
, boost::optional
<DomainInfo::DomainKind
>& kind
, boost::optional
<vector
<ComboAddress
>>& primaries
, boost::optional
<DNSName
>& catalog
, boost::optional
<string
>& account
)
715 if (document
["kind"].is_string()) {
716 kind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
722 if (document
["masters"].is_array()) {
723 primaries
= vector
<ComboAddress
>();
724 for (const auto& value
: document
["masters"].array_items()) {
725 string primary
= value
.string_value();
726 if (primary
.empty()) {
727 throw ApiException("Primary can not be an empty string");
730 primaries
->emplace_back(primary
, 53);
732 catch (const PDNSException
& e
) {
733 throw ApiException("Primary (" + primary
+ ") is not an IP address: " + e
.reason
);
738 primaries
= boost::none
;
741 if (document
["catalog"].is_string()) {
742 string catstring
= document
["catalog"].string_value();
743 catalog
= (!catstring
.empty() ? DNSName(catstring
) : DNSName());
746 catalog
= boost::none
;
749 if (document
["account"].is_string()) {
750 account
= document
["account"].string_value();
753 account
= boost::none
;
758 * Build vector of TSIG Key ids from domain update document.
759 * jsonArray: JSON array element to extract TSIG key ids from.
760 * metadata: returned list of domain key ids for setDomainMetadata
762 static void extractJsonTSIGKeyIds(UeberBackend
& backend
, const Json
& jsonArray
, vector
<string
>& metadata
)
764 for (const auto& value
: jsonArray
.array_items()) {
765 auto keyname(apiZoneIdToName(value
.string_value()));
768 if (!backend
.getTSIGKey(keyname
, keyAlgo
, keyContent
)) {
769 throw ApiException("A TSIG key with the name '" + keyname
.toLogString() + "' does not exist");
771 metadata
.push_back(keyname
.toString());
775 // Must be called within backend transaction.
776 static void updateDomainSettingsFromDocument(UeberBackend
& backend
, DomainInfo
& domainInfo
, const DNSName
& zonename
, const Json
& document
, bool zoneWasModified
)
778 boost::optional
<DomainInfo::DomainKind
> kind
;
779 boost::optional
<vector
<ComboAddress
>> primaries
;
780 boost::optional
<DNSName
> catalog
;
781 boost::optional
<string
> account
;
783 extractDomainInfoFromDocument(document
, kind
, primaries
, catalog
, account
);
786 domainInfo
.backend
->setKind(zonename
, *kind
);
787 domainInfo
.kind
= *kind
;
790 domainInfo
.backend
->setPrimaries(zonename
, *primaries
);
793 domainInfo
.backend
->setCatalog(zonename
, *catalog
);
796 domainInfo
.backend
->setAccount(zonename
, *account
);
799 if (document
["soa_edit_api"].is_string()) {
800 domainInfo
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", document
["soa_edit_api"].string_value());
802 if (document
["soa_edit"].is_string()) {
803 domainInfo
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT", document
["soa_edit"].string_value());
806 bool api_rectify
= boolFromJson(document
, "api_rectify");
807 domainInfo
.backend
->setDomainMetadataOne(zonename
, "API-RECTIFY", api_rectify
? "1" : "0");
809 catch (const JsonException
&) {
812 DNSSECKeeper
dnssecKeeper(&backend
);
813 bool shouldRectify
= zoneWasModified
;
814 bool dnssecInJSON
= false;
815 bool dnssecDocVal
= false;
816 bool nsec3paramInJSON
= false;
817 bool updateNsec3Param
= false;
818 string nsec3paramDocVal
;
821 dnssecDocVal
= boolFromJson(document
, "dnssec");
824 catch (const JsonException
&) {
828 nsec3paramDocVal
= stringFromJson(document
, "nsec3param");
829 nsec3paramInJSON
= true;
831 catch (const JsonException
&) {
834 bool isDNSSECZone
= dnssecKeeper
.isSecuredZone(zonename
);
835 bool isPresigned
= dnssecKeeper
.isPresigned(zonename
);
840 addDefaultDNSSECKeys(dnssecKeeper
, zonename
);
842 // Used later for NSEC3PARAM
843 isDNSSECZone
= dnssecKeeper
.isSecuredZone(zonename
);
846 throwUnableToSecure(zonename
);
848 shouldRectify
= true;
849 updateNsec3Param
= true;
853 // "dnssec": false in json
857 if (!dnssecKeeper
.unSecureZone(zonename
, error
)) {
858 throw ApiException("Error while un-securing zone '" + zonename
.toString() + "': " + error
);
860 isDNSSECZone
= dnssecKeeper
.isSecuredZone(zonename
, false);
862 throw ApiException("Unable to un-secure zone '" + zonename
.toString() + "'");
864 shouldRectify
= true;
865 updateNsec3Param
= true;
870 if (nsec3paramInJSON
|| updateNsec3Param
) {
871 shouldRectify
= true;
872 if (!isDNSSECZone
&& !nsec3paramDocVal
.empty()) {
873 throw ApiException("NSEC3PARAM value provided for zone '" + zonename
.toString() + "', but zone is not DNSSEC secured.");
876 if (nsec3paramDocVal
.empty()) {
878 if (!dnssecKeeper
.unsetNSEC3PARAM(zonename
)) {
879 throw ApiException("Unable to remove NSEC3PARAMs from zone '" + zonename
.toString());
883 // Set the NSEC3PARAMs
884 NSEC3PARAMRecordContent
ns3pr(nsec3paramDocVal
);
886 if (!dnssecKeeper
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
887 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() + "' are invalid. " + error_msg
);
889 if (!dnssecKeeper
.setNSEC3PARAM(zonename
, ns3pr
, boolFromJson(document
, "nsec3narrow", false))) {
890 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() + "' passed our basic sanity checks, but cannot be used with the current backend.");
895 if (shouldRectify
&& !isPresigned
) {
897 if (isZoneApiRectifyEnabled(domainInfo
)) {
900 if (!dnssecKeeper
.rectifyZone(zonename
, error_msg
, info
, false) && !domainInfo
.isSecondaryType()) {
901 // for Secondary zones, it is possible that rectifying was not needed (example: empty zone).
902 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
907 string soa_edit_api_kind
;
908 domainInfo
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
909 if (!soa_edit_api_kind
.empty()) {
911 if (!backend
.getSOAUncached(zonename
, soaData
)) {
915 string soa_edit_kind
;
916 domainInfo
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
918 DNSResourceRecord resourceRecord
;
919 if (makeIncreasedSOARecord(soaData
, soa_edit_api_kind
, soa_edit_kind
, resourceRecord
)) {
920 if (!domainInfo
.backend
->replaceRRSet(domainInfo
.id
, resourceRecord
.qname
, resourceRecord
.qtype
, vector
<DNSResourceRecord
>(1, resourceRecord
))) {
921 throw ApiException("Hosting backend does not support editing records.");
927 if (!document
["master_tsig_key_ids"].is_null()) {
928 vector
<string
> metadata
;
929 extractJsonTSIGKeyIds(backend
, document
["master_tsig_key_ids"], metadata
);
930 if (!domainInfo
.backend
->setDomainMetadata(zonename
, "TSIG-ALLOW-AXFR", metadata
)) {
931 throw HttpInternalServerErrorException("Unable to set new TSIG primary keys for zone '" + zonename
.toLogString() + "'");
934 if (!document
["slave_tsig_key_ids"].is_null()) {
935 vector
<string
> metadata
;
936 extractJsonTSIGKeyIds(backend
, document
["slave_tsig_key_ids"], metadata
);
937 if (!domainInfo
.backend
->setDomainMetadata(zonename
, "AXFR-MASTER-TSIG", metadata
)) {
938 throw HttpInternalServerErrorException("Unable to set new TSIG secondary keys for zone '" + zonename
.toLogString() + "'");
943 static bool isValidMetadataKind(const string
& kind
, bool readonly
)
945 static vector
<string
> builtinOptions
{
948 "ALLOW-DNSUPDATE-FROM",
949 "TSIG-ALLOW-DNSUPDATE",
951 "SOA-EDIT-DNSUPDATE",
955 "GSS-ALLOW-AXFR-PRINCIPAL",
956 "GSS-ACCEPTOR-PRINCIPAL",
967 "TSIG-ALLOW-DNSUPDATE",
970 // the following options do not allow modifications via API
971 static vector
<string
> protectedOptions
{
981 if (kind
.find("X-") == 0) {
987 for (const string
& builtinOption
: builtinOptions
) {
988 if (kind
== builtinOption
) {
989 for (const string
& protectedOption
: protectedOptions
) {
990 if (!readonly
&& builtinOption
== protectedOption
) {
1002 /* Return OpenAPI document describing the supported API.
1004 #include "apidocfiles.h"
1006 void apiDocs(HttpRequest
* req
, HttpResponse
* resp
)
1008 if (req
->accept_yaml
) {
1009 resp
->setYamlBody(g_api_swagger_yaml
);
1011 else if (req
->accept_json
) {
1012 resp
->setJsonBody(g_api_swagger_json
);
1015 resp
->setPlainBody(g_api_swagger_yaml
);
1022 ZoneData(HttpRequest
* req
) :
1023 zoneName(apiZoneIdToName((req
)->parameters
["id"])),
1024 dnssecKeeper(DNSSECKeeper
{&backend
})
1027 if (!backend
.getDomainInfo(zoneName
, domainInfo
)) {
1028 throw HttpNotFoundException();
1031 catch (const PDNSException
& e
) {
1032 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1037 UeberBackend backend
{};
1038 DNSSECKeeper dnssecKeeper
;
1039 DomainInfo domainInfo
{};
1042 static void apiZoneMetadataGET(HttpRequest
* req
, HttpResponse
* resp
)
1044 ZoneData zoneData
{req
};
1046 map
<string
, vector
<string
>> metas
;
1047 Json::array document
;
1049 if (!zoneData
.backend
.getAllDomainMetadata(zoneData
.zoneName
, metas
)) {
1050 throw HttpNotFoundException();
1053 for (const auto& meta
: metas
) {
1054 Json::array entries
;
1055 for (const string
& value
: meta
.second
) {
1056 entries
.emplace_back(value
);
1060 {"type", "Metadata"},
1061 {"kind", meta
.first
},
1062 {"metadata", entries
}};
1063 document
.emplace_back(key
);
1065 resp
->setJsonBody(document
);
1068 static void apiZoneMetadataPOST(HttpRequest
* req
, HttpResponse
* resp
)
1070 ZoneData zoneData
{req
};
1072 const auto& document
= req
->json();
1074 vector
<string
> entries
;
1077 kind
= stringFromJson(document
, "kind");
1079 catch (const JsonException
&) {
1080 throw ApiException("kind is not specified or not a string");
1083 if (!isValidMetadataKind(kind
, false)) {
1084 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1087 vector
<string
> vecMetadata
;
1089 if (!zoneData
.backend
.getDomainMetadata(zoneData
.zoneName
, kind
, vecMetadata
)) {
1090 throw ApiException("Could not retrieve metadata entries for domain '" + zoneData
.zoneName
.toString() + "'");
1093 const auto& metadata
= document
["metadata"];
1094 if (!metadata
.is_array()) {
1095 throw ApiException("metadata is not specified or not an array");
1098 for (const auto& value
: metadata
.array_items()) {
1099 if (!value
.is_string()) {
1100 throw ApiException("metadata must be strings");
1102 if (std::find(vecMetadata
.cbegin(),
1104 value
.string_value())
1105 == vecMetadata
.cend()) {
1106 vecMetadata
.push_back(value
.string_value());
1110 if (!zoneData
.backend
.setDomainMetadata(zoneData
.zoneName
, kind
, vecMetadata
)) {
1111 throw ApiException("Could not update metadata entries for domain '" + zoneData
.zoneName
.toString() + "'");
1114 DNSSECKeeper::clearMetaCache(zoneData
.zoneName
);
1116 Json::array respMetadata
;
1117 for (const string
& value
: vecMetadata
) {
1118 respMetadata
.emplace_back(value
);
1122 {"type", "Metadata"},
1123 {"kind", document
["kind"]},
1124 {"metadata", respMetadata
}};
1127 resp
->setJsonBody(key
);
1130 static void apiZoneMetadataKindGET(HttpRequest
* req
, HttpResponse
* resp
)
1132 ZoneData zoneData
{req
};
1134 string kind
= req
->parameters
["kind"];
1136 vector
<string
> metadata
;
1137 Json::object document
;
1138 Json::array entries
;
1140 if (!zoneData
.backend
.getDomainMetadata(zoneData
.zoneName
, kind
, metadata
)) {
1141 throw HttpNotFoundException();
1143 if (!isValidMetadataKind(kind
, true)) {
1144 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1147 document
["type"] = "Metadata";
1148 document
["kind"] = kind
;
1150 for (const string
& value
: metadata
) {
1151 entries
.emplace_back(value
);
1154 document
["metadata"] = entries
;
1155 resp
->setJsonBody(document
);
1158 static void apiZoneMetadataKindPUT(HttpRequest
* req
, HttpResponse
* resp
)
1160 ZoneData zoneData
{req
};
1162 string kind
= req
->parameters
["kind"];
1164 const auto& document
= req
->json();
1166 if (!isValidMetadataKind(kind
, false)) {
1167 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1170 vector
<string
> vecMetadata
;
1171 const auto& metadata
= document
["metadata"];
1172 if (!metadata
.is_array()) {
1173 throw ApiException("metadata is not specified or not an array");
1175 for (const auto& value
: metadata
.array_items()) {
1176 if (!value
.is_string()) {
1177 throw ApiException("metadata must be strings");
1179 vecMetadata
.push_back(value
.string_value());
1182 if (!zoneData
.backend
.setDomainMetadata(zoneData
.zoneName
, kind
, vecMetadata
)) {
1183 throw ApiException("Could not update metadata entries for domain '" + zoneData
.zoneName
.toString() + "'");
1186 DNSSECKeeper::clearMetaCache(zoneData
.zoneName
);
1189 {"type", "Metadata"},
1191 {"metadata", metadata
}};
1193 resp
->setJsonBody(key
);
1196 static void apiZoneMetadataKindDELETE(HttpRequest
* req
, HttpResponse
* resp
)
1198 ZoneData zoneData
{req
};
1200 const string
& kind
= req
->parameters
["kind"];
1201 if (!isValidMetadataKind(kind
, false)) {
1202 throw ApiException("Unsupported metadata kind '" + kind
+ "'");
1205 vector
<string
> metadata
; // an empty vector will do it
1206 if (!zoneData
.backend
.setDomainMetadata(zoneData
.zoneName
, kind
, metadata
)) {
1207 throw ApiException("Could not delete metadata for domain '" + zoneData
.zoneName
.toString() + "' (" + kind
+ ")");
1210 DNSSECKeeper::clearMetaCache(zoneData
.zoneName
);
1214 // Throws 404 if the key with inquireKeyId does not exist
1215 static void apiZoneCryptoKeysCheckKeyExists(const DNSName
& zonename
, int inquireKeyId
, DNSSECKeeper
* dnssecKeeper
)
1217 DNSSECKeeper::keyset_t keyset
= dnssecKeeper
->getKeys(zonename
, false);
1219 for (const auto& value
: keyset
) {
1220 if (value
.second
.id
== (unsigned)inquireKeyId
) {
1226 throw HttpNotFoundException();
1230 static inline int getInquireKeyId(HttpRequest
* req
, const DNSName
& zonename
, DNSSECKeeper
* dnsseckeeper
)
1232 int inquireKeyId
= -1;
1233 if (req
->parameters
.count("key_id") == 1) {
1234 inquireKeyId
= std::stoi(req
->parameters
["key_id"]);
1235 apiZoneCryptoKeysCheckKeyExists(zonename
, inquireKeyId
, dnsseckeeper
);
1237 return inquireKeyId
;
1240 static void apiZoneCryptokeysExport(const DNSName
& zonename
, int64_t inquireKeyId
, HttpResponse
* resp
, DNSSECKeeper
* dnssec_dk
)
1242 DNSSECKeeper::keyset_t keyset
= dnssec_dk
->getKeys(zonename
, false);
1244 bool inquireSingleKey
= inquireKeyId
>= 0;
1247 for (const auto& value
: keyset
) {
1248 if (inquireSingleKey
&& (unsigned)inquireKeyId
!= value
.second
.id
) {
1253 switch (value
.second
.keyType
) {
1254 case DNSSECKeeper::KSK
:
1257 case DNSSECKeeper::ZSK
:
1260 case DNSSECKeeper::CSK
:
1266 {"type", "Cryptokey"},
1267 {"id", static_cast<int>(value
.second
.id
)},
1268 {"active", value
.second
.active
},
1269 {"published", value
.second
.published
},
1270 {"keytype", keyType
},
1271 {"flags", static_cast<uint16_t>(value
.first
.getFlags())},
1272 {"dnskey", value
.first
.getDNSKEY().getZoneRepresentation()},
1273 {"algorithm", DNSSECKeeper::algorithm2name(value
.first
.getAlgorithm())},
1274 {"bits", value
.first
.getKey()->getBits()}};
1277 dnssec_dk
->getPublishCDS(zonename
, publishCDS
);
1279 vector
<string
> digestAlgos
;
1280 stringtok(digestAlgos
, publishCDS
, ", ");
1282 std::set
<unsigned int> CDSalgos
;
1283 for (auto const& digestAlgo
: digestAlgos
) {
1284 CDSalgos
.insert(pdns::checked_stoi
<unsigned int>(digestAlgo
));
1287 if (value
.second
.keyType
== DNSSECKeeper::KSK
|| value
.second
.keyType
== DNSSECKeeper::CSK
) {
1290 for (const uint8_t keyid
: {DNSSECKeeper::DIGEST_SHA256
, DNSSECKeeper::DIGEST_SHA384
}) {
1292 string dsRecordContent
= makeDSFromDNSKey(zonename
, value
.first
.getDNSKEY(), keyid
).getZoneRepresentation();
1294 dses
.emplace_back(dsRecordContent
);
1296 if (CDSalgos
.count(keyid
) != 0) {
1297 cdses
.emplace_back(dsRecordContent
);
1306 if (!cdses
.empty()) {
1311 if (inquireSingleKey
) {
1312 key
["privatekey"] = value
.first
.getKey()->convertToISC();
1313 resp
->setJsonBody(key
);
1316 doc
.emplace_back(key
);
1319 if (inquireSingleKey
) {
1320 // we came here because we couldn't find the requested key.
1321 throw HttpNotFoundException();
1323 resp
->setJsonBody(doc
);
1326 static void apiZoneCryptokeysGET(HttpRequest
* req
, HttpResponse
* resp
)
1328 ZoneData zoneData
{req
};
1329 const auto inquireKeyId
= getInquireKeyId(req
, zoneData
.zoneName
, &zoneData
.dnssecKeeper
);
1331 apiZoneCryptokeysExport(zoneData
.zoneName
, inquireKeyId
, resp
, &zoneData
.dnssecKeeper
);
1335 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1336 * It deletes a key from :zone_name specified by :cryptokey_id.
1338 * Case 1: the backend returns true on removal. This means the key is gone.
1339 * The server returns 204 No Content, no body.
1340 * Case 2: the backend returns false on removal. An error occurred.
1341 * The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1342 * Case 3: the key or zone does not exist.
1343 * The server returns 404 Not Found
1345 static void apiZoneCryptokeysDELETE(HttpRequest
* req
, HttpResponse
* resp
)
1347 ZoneData zoneData
{req
};
1348 const auto inquireKeyId
= getInquireKeyId(req
, zoneData
.zoneName
, &zoneData
.dnssecKeeper
);
1350 if (inquireKeyId
== -1) {
1351 throw HttpBadRequestException();
1354 if (zoneData
.dnssecKeeper
.removeKey(zoneData
.zoneName
, inquireKeyId
)) {
1359 resp
->setErrorResult("Could not DELETE " + req
->parameters
["key_id"], 422);
1364 * This method adds a key to a zone by generate it or content parameter.
1367 * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1368 * "keytype" : "ksk|zsk" <string>
1369 * "active" : "true|false" <value>
1370 * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1371 * "bits" : number of bits <int>
1375 * Case 1: keytype isn't ksk|zsk
1376 * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1377 * Case 2: 'bits' must be a positive integer value.
1378 * The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1379 * Case 3: The "algorithm" isn't supported
1380 * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1381 * Case 4: Algorithm <= 10 and no bits were passed
1382 * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1383 * Case 5: The wrong keysize was passed
1384 * The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1385 * Case 6: If the server cant guess the keysize
1386 * The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1387 * Case 7: The key-creation failed
1388 * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1389 * Case 8: The key in content has the wrong format
1390 * The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1391 * Case 9: The wrong combination of fields is submitted
1392 * The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1393 * Case 10: No content and everything was fine
1394 * The server returns 201 Created and all public data about the new cryptokey
1395 * Case 11: With specified content
1396 * The server returns 201 Created and all public data about the added cryptokey
1399 static void apiZoneCryptokeysPOST(HttpRequest
* req
, HttpResponse
* resp
)
1401 ZoneData zoneData
{req
};
1403 const auto& document
= req
->json();
1404 string privatekey_fieldname
= "privatekey";
1405 auto privatekey
= document
["privatekey"];
1406 if (privatekey
.is_null()) {
1407 // Fallback to the old "content" behaviour
1408 privatekey
= document
["content"];
1409 privatekey_fieldname
= "content";
1411 bool active
= boolFromJson(document
, "active", false);
1412 bool published
= boolFromJson(document
, "published", true);
1413 bool keyOrZone
= false;
1415 if (stringFromJson(document
, "keytype") == "ksk" || stringFromJson(document
, "keytype") == "csk") {
1418 else if (stringFromJson(document
, "keytype") == "zsk") {
1422 throw ApiException("Invalid keytype " + stringFromJson(document
, "keytype"));
1425 int64_t insertedId
= -1;
1427 if (privatekey
.is_null()) {
1428 int bits
= keyOrZone
? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
1429 auto docbits
= document
["bits"];
1430 if (!docbits
.is_null()) {
1431 if (!docbits
.is_number() || (fmod(docbits
.number_value(), 1.0) != 0) || docbits
.int_value() < 0) {
1432 throw ApiException("'bits' must be a positive integer value");
1435 bits
= docbits
.int_value();
1437 int algorithm
= DNSSECKeeper::shorthand2algorithm(keyOrZone
? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
1438 const auto& providedAlgo
= document
["algorithm"];
1439 if (providedAlgo
.is_string()) {
1440 algorithm
= DNSSECKeeper::shorthand2algorithm(providedAlgo
.string_value());
1441 if (algorithm
== -1) {
1442 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1445 else if (providedAlgo
.is_number()) {
1446 algorithm
= providedAlgo
.int_value();
1448 else if (!providedAlgo
.is_null()) {
1449 throw ApiException("Unknown algorithm: " + providedAlgo
.string_value());
1453 if (!zoneData
.dnssecKeeper
.addKey(zoneData
.zoneName
, keyOrZone
, algorithm
, insertedId
, bits
, active
, published
)) {
1454 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1457 catch (std::runtime_error
& error
) {
1458 throw ApiException(error
.what());
1460 if (insertedId
< 0) {
1461 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1464 else if (document
["bits"].is_null() && document
["algorithm"].is_null()) {
1465 const auto& keyData
= stringFromJson(document
, privatekey_fieldname
);
1466 DNSKEYRecordContent dkrc
;
1467 DNSSECPrivateKey dpk
;
1469 shared_ptr
<DNSCryptoKeyEngine
> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc
, keyData
));
1478 uint8_t algorithm
= dkrc
.d_algorithm
;
1479 // TODO remove in 4.2.0
1480 if (algorithm
== DNSSECKeeper::RSASHA1NSEC3SHA1
) {
1481 algorithm
= DNSSECKeeper::RSASHA1
;
1483 dpk
.setKey(dke
, flags
, algorithm
);
1485 catch (std::runtime_error
& error
) {
1486 throw ApiException("Key could not be parsed. Make sure your key format is correct.");
1489 if (!zoneData
.dnssecKeeper
.addKey(zoneData
.zoneName
, dpk
, insertedId
, active
, published
)) {
1490 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1493 catch (std::runtime_error
& error
) {
1494 throw ApiException(error
.what());
1496 if (insertedId
< 0) {
1497 throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
1501 throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
1503 apiZoneCryptokeysExport(zoneData
.zoneName
, insertedId
, resp
, &zoneData
.dnssecKeeper
);
1508 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1509 * It de/activates a key from :zone_name specified by :cryptokey_id.
1511 * Case 1: invalid JSON data
1512 * The server returns 400 Bad Request
1513 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1514 * The server returns 204 No Content
1515 * Case 3: the backend returns false on de/activation. An error occurred.
1516 * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1518 static void apiZoneCryptokeysPUT(HttpRequest
* req
, HttpResponse
* resp
)
1520 ZoneData zoneData
{req
};
1521 const auto inquireKeyId
= getInquireKeyId(req
, zoneData
.zoneName
, &zoneData
.dnssecKeeper
);
1523 if (inquireKeyId
== -1) {
1524 throw HttpBadRequestException();
1526 // throws an exception if the Body is empty
1527 const auto& document
= req
->json();
1528 // throws an exception if the key does not exist or is not a bool
1529 bool active
= boolFromJson(document
, "active");
1530 bool published
= boolFromJson(document
, "published", true);
1532 if (!zoneData
.dnssecKeeper
.activateKey(zoneData
.zoneName
, inquireKeyId
)) {
1533 resp
->setErrorResult("Could not activate Key: " + req
->parameters
["key_id"] + " in Zone: " + zoneData
.zoneName
.toString(), 422);
1538 if (!zoneData
.dnssecKeeper
.deactivateKey(zoneData
.zoneName
, inquireKeyId
)) {
1539 resp
->setErrorResult("Could not deactivate Key: " + req
->parameters
["key_id"] + " in Zone: " + zoneData
.zoneName
.toString(), 422);
1545 if (!zoneData
.dnssecKeeper
.publishKey(zoneData
.zoneName
, inquireKeyId
)) {
1546 resp
->setErrorResult("Could not publish Key: " + req
->parameters
["key_id"] + " in Zone: " + zoneData
.zoneName
.toString(), 422);
1551 if (!zoneData
.dnssecKeeper
.unpublishKey(zoneData
.zoneName
, inquireKeyId
)) {
1552 resp
->setErrorResult("Could not unpublish Key: " + req
->parameters
["key_id"] + " in Zone: " + zoneData
.zoneName
.toString(), 422);
1561 static void gatherRecordsFromZone(const std::string
& zonestring
, vector
<DNSResourceRecord
>& new_records
, const DNSName
& zonename
)
1563 DNSResourceRecord resourceRecord
;
1564 vector
<string
> zonedata
;
1565 stringtok(zonedata
, zonestring
, "\r\n");
1567 ZoneParserTNG
zpt(zonedata
, zonename
);
1568 zpt
.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
1569 zpt
.setMaxIncludes(::arg().asNum("max-include-depth"));
1571 bool seenSOA
= false;
1573 string comment
= "Imported via the API";
1576 while (zpt
.get(resourceRecord
, &comment
)) {
1577 if (seenSOA
&& resourceRecord
.qtype
.getCode() == QType::SOA
) {
1580 if (resourceRecord
.qtype
.getCode() == QType::SOA
) {
1583 validateGatheredRRType(resourceRecord
);
1585 new_records
.push_back(resourceRecord
);
1588 catch (std::exception
& ae
) {
1589 throw ApiException("An error occurred while parsing the zonedata: " + string(ae
.what()));
1593 /** Throws ApiException if records which violate RRset constraints are present.
1594 * NOTE: sorts records in-place.
1596 * Constraints being checked:
1597 * *) no exact duplicates
1598 * *) no duplicates for QTypes that can only be present once per RRset
1599 * *) hostnames are hostnames
1601 static void checkNewRecords(vector
<DNSResourceRecord
>& records
, const DNSName
& zone
)
1603 sort(records
.begin(), records
.end(),
1604 [](const DNSResourceRecord
& rec_a
, const DNSResourceRecord
& rec_b
) -> bool {
1605 /* we need _strict_ weak ordering */
1606 return std::tie(rec_a
.qname
, rec_a
.qtype
, rec_a
.content
) < std::tie(rec_b
.qname
, rec_b
.qtype
, rec_b
.content
);
1609 DNSResourceRecord previous
;
1610 for (const auto& rec
: records
) {
1611 if (previous
.qname
== rec
.qname
) {
1612 if (previous
.qtype
== rec
.qtype
) {
1613 if (onlyOneEntryTypes
.count(rec
.qtype
.getCode()) != 0) {
1614 throw ApiException("RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.toString() + " has more than one record");
1616 if (previous
.content
== rec
.content
) {
1617 throw ApiException("Duplicate record in RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.toString() + " with content \"" + rec
.content
+ "\"");
1620 else if (exclusiveEntryTypes
.count(rec
.qtype
.getCode()) != 0 || exclusiveEntryTypes
.count(previous
.qtype
.getCode()) != 0) {
1621 throw ApiException("RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.toString() + ": Conflicts with another RRset");
1625 if (rec
.qname
== zone
) {
1626 if (nonApexTypes
.count(rec
.qtype
.getCode()) != 0) {
1627 throw ApiException("Record " + rec
.qname
.toString() + " IN " + rec
.qtype
.toString() + " is not allowed at apex");
1630 else if (atApexTypes
.count(rec
.qtype
.getCode()) != 0) {
1631 throw ApiException("Record " + rec
.qname
.toString() + " IN " + rec
.qtype
.toString() + " is only allowed at apex");
1634 // Check if the DNSNames that should be hostnames, are hostnames
1636 checkHostnameCorrectness(rec
);
1638 catch (const std::exception
& e
) {
1639 throw ApiException("RRset " + rec
.qname
.toString() + " IN " + rec
.qtype
.toString() + ": " + e
.what());
1646 static void checkTSIGKey(UeberBackend
& backend
, const DNSName
& keyname
, const DNSName
& algo
, const string
& content
)
1649 string contentFromDB
;
1650 if (backend
.getTSIGKey(keyname
, algoFromDB
, contentFromDB
)) {
1651 throw HttpConflictException("A TSIG key with the name '" + keyname
.toLogString() + "' already exists");
1655 if (!getTSIGHashEnum(algo
, the
)) {
1656 throw ApiException("Unknown TSIG algorithm: " + algo
.toLogString());
1660 if (B64Decode(content
, b64out
) == -1) {
1661 throw ApiException("TSIG content '" + content
+ "' cannot be base64-decoded");
1665 static Json::object
makeJSONTSIGKey(const DNSName
& keyname
, const DNSName
& algo
, const string
& content
)
1667 Json::object tsigkey
= {
1668 {"name", keyname
.toStringNoDot()},
1669 {"id", apiZoneNameToId(keyname
)},
1670 {"algorithm", algo
.toStringNoDot()},
1672 {"type", "TSIGKey"}};
1676 static Json::object
makeJSONTSIGKey(const struct TSIGKey
& key
, bool doContent
= true)
1678 return makeJSONTSIGKey(key
.name
, key
.algorithm
, doContent
? key
.key
: "");
1681 static void apiServerTSIGKeysGET(HttpRequest
* /* req */, HttpResponse
* resp
)
1683 UeberBackend backend
;
1684 vector
<struct TSIGKey
> keys
;
1686 if (!backend
.getTSIGKeys(keys
)) {
1687 throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
1692 for (const auto& key
: keys
) {
1693 doc
.emplace_back(makeJSONTSIGKey(key
, false));
1695 resp
->setJsonBody(doc
);
1698 static void apiServerTSIGKeysPOST(HttpRequest
* req
, HttpResponse
* resp
)
1700 UeberBackend backend
;
1701 const auto& document
= req
->json();
1702 DNSName
keyname(stringFromJson(document
, "name"));
1703 DNSName
algo(stringFromJson(document
, "algorithm"));
1704 string content
= document
["key"].string_value();
1706 if (content
.empty()) {
1708 content
= makeTSIGKey(algo
);
1710 catch (const PDNSException
& exc
) {
1711 throw HttpBadRequestException(exc
.reason
);
1715 // Will throw an ApiException or HttpConflictException on error
1716 checkTSIGKey(backend
, keyname
, algo
, content
);
1718 if (!backend
.setTSIGKey(keyname
, algo
, content
)) {
1719 throw HttpInternalServerErrorException("Unable to add TSIG key");
1723 resp
->setJsonBody(makeJSONTSIGKey(keyname
, algo
, content
));
1729 TSIGKeyData(HttpRequest
* req
) :
1730 keyName(apiZoneIdToName(req
->parameters
["id"]))
1733 if (!backend
.getTSIGKey(keyName
, algo
, content
)) {
1734 throw HttpNotFoundException("TSIG key with name '" + keyName
.toLogString() + "' not found");
1737 catch (const PDNSException
& e
) {
1738 throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e
.reason
);
1741 tsigKey
.name
= keyName
;
1742 tsigKey
.algorithm
= algo
;
1743 tsigKey
.key
= std::move(content
);
1746 UeberBackend backend
;
1750 struct TSIGKey tsigKey
;
1753 static void apiServerTSIGKeyDetailGET(HttpRequest
* req
, HttpResponse
* resp
)
1755 TSIGKeyData tsigKeyData
{req
};
1757 resp
->setJsonBody(makeJSONTSIGKey(tsigKeyData
.tsigKey
));
1760 static void apiServerTSIGKeyDetailPUT(HttpRequest
* req
, HttpResponse
* resp
)
1762 TSIGKeyData tsigKeyData
{req
};
1764 const auto& document
= req
->json();
1766 if (document
["name"].is_string()) {
1767 tsigKeyData
.tsigKey
.name
= DNSName(document
["name"].string_value());
1769 if (document
["algorithm"].is_string()) {
1770 tsigKeyData
.tsigKey
.algorithm
= DNSName(document
["algorithm"].string_value());
1773 if (!getTSIGHashEnum(tsigKeyData
.tsigKey
.algorithm
, the
)) {
1774 throw ApiException("Unknown TSIG algorithm: " + tsigKeyData
.tsigKey
.algorithm
.toLogString());
1777 if (document
["key"].is_string()) {
1778 string new_content
= document
["key"].string_value();
1780 if (B64Decode(new_content
, decoded
) == -1) {
1781 throw ApiException("Can not base64 decode key content '" + new_content
+ "'");
1783 tsigKeyData
.tsigKey
.key
= std::move(new_content
);
1785 if (!tsigKeyData
.backend
.setTSIGKey(tsigKeyData
.tsigKey
.name
, tsigKeyData
.tsigKey
.algorithm
, tsigKeyData
.tsigKey
.key
)) {
1786 throw HttpInternalServerErrorException("Unable to save TSIG Key");
1788 if (tsigKeyData
.tsigKey
.name
!= tsigKeyData
.keyName
) {
1789 // Remove the old key
1790 if (!tsigKeyData
.backend
.deleteTSIGKey(tsigKeyData
.keyName
)) {
1791 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData
.keyName
.toStringNoDot() + "'");
1794 resp
->setJsonBody(makeJSONTSIGKey(tsigKeyData
.tsigKey
));
1797 static void apiServerTSIGKeyDetailDELETE(HttpRequest
* req
, HttpResponse
* resp
)
1799 TSIGKeyData tsigKeyData
{req
};
1800 if (!tsigKeyData
.backend
.deleteTSIGKey(tsigKeyData
.keyName
)) {
1801 throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData
.keyName
.toStringNoDot() + "'");
1807 static void apiServerAutoprimaryDetailDELETE(HttpRequest
* req
, HttpResponse
* resp
)
1809 UeberBackend backend
;
1810 const AutoPrimary
& primary
{req
->parameters
["ip"], req
->parameters
["nameserver"], ""};
1811 if (!backend
.autoPrimaryRemove(primary
)) {
1812 throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
1818 static void apiServerAutoprimariesGET(HttpRequest
* /* req */, HttpResponse
* resp
)
1820 UeberBackend backend
;
1822 std::vector
<AutoPrimary
> primaries
;
1823 if (!backend
.autoPrimariesList(primaries
)) {
1824 throw HttpInternalServerErrorException("Unable to retrieve autoprimaries");
1827 for (const auto& primary
: primaries
) {
1828 const Json::object obj
= {
1830 {"nameserver", primary
.nameserver
},
1831 {"account", primary
.account
}};
1832 doc
.emplace_back(obj
);
1834 resp
->setJsonBody(doc
);
1837 static void apiServerAutoprimariesPOST(HttpRequest
* req
, HttpResponse
* resp
)
1839 UeberBackend backend
;
1841 const auto& document
= req
->json();
1843 AutoPrimary
primary(stringFromJson(document
, "ip"), stringFromJson(document
, "nameserver"), "");
1845 if (document
["account"].is_string()) {
1846 primary
.account
= document
["account"].string_value();
1849 if (primary
.ip
.empty() or primary
.nameserver
.empty()) {
1850 throw ApiException("ip and nameserver fields must be filled");
1852 if (!backend
.autoPrimaryAdd(primary
)) {
1853 throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
1860 static void apiServerZonesPOST(HttpRequest
* req
, HttpResponse
* resp
)
1862 UeberBackend backend
;
1863 DNSSECKeeper
dnssecKeeper(&backend
);
1864 DomainInfo domainInfo
;
1865 const auto& document
= req
->json();
1866 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
1867 apiCheckNameAllowedCharacters(zonename
.toString());
1868 zonename
.makeUsLowerCase();
1870 bool exists
= backend
.getDomainInfo(zonename
, domainInfo
);
1872 throw HttpConflictException();
1875 boost::optional
<DomainInfo::DomainKind
> kind
;
1876 boost::optional
<vector
<ComboAddress
>> primaries
;
1877 boost::optional
<DNSName
> catalog
;
1878 boost::optional
<string
> account
;
1879 extractDomainInfoFromDocument(document
, kind
, primaries
, catalog
, account
);
1881 // validate 'kind' is set
1883 throw JsonException("Key 'kind' not present or not a String");
1885 DomainInfo::DomainKind zonekind
= *kind
;
1887 string zonestring
= document
["zone"].string_value();
1888 auto rrsets
= document
["rrsets"];
1889 if (rrsets
.is_array() && !zonestring
.empty()) {
1890 throw ApiException("You cannot give rrsets AND zone data as text");
1893 const auto& nameservers
= document
["nameservers"];
1894 if (!nameservers
.is_null() && !nameservers
.is_array() && zonekind
!= DomainInfo::Secondary
&& zonekind
!= DomainInfo::Consumer
) {
1895 throw ApiException("Nameservers is not a list");
1898 // if records/comments are given, load and check them
1899 bool have_soa
= false;
1900 bool have_zone_ns
= false;
1901 vector
<DNSResourceRecord
> new_records
;
1902 vector
<Comment
> new_comments
;
1905 if (rrsets
.is_array()) {
1906 for (const auto& rrset
: rrsets
.array_items()) {
1907 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
1908 apiCheckQNameAllowedCharacters(qname
.toString());
1910 qtype
= stringFromJson(rrset
, "type");
1911 if (qtype
.getCode() == 0) {
1912 throw ApiException("RRset " + qname
.toString() + " IN " + stringFromJson(rrset
, "type") + ": unknown type given");
1914 if (rrset
["records"].is_array()) {
1915 uint32_t ttl
= uintFromJson(rrset
, "ttl");
1916 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
);
1918 if (rrset
["comments"].is_array()) {
1919 gatherComments(rrset
, qname
, qtype
, new_comments
);
1923 else if (!zonestring
.empty()) {
1924 gatherRecordsFromZone(zonestring
, new_records
, zonename
);
1927 catch (const JsonException
& exc
) {
1928 throw ApiException("New RRsets are invalid: " + string(exc
.what()));
1931 if (zonekind
== DomainInfo::Consumer
&& !new_records
.empty()) {
1932 throw ApiException("Zone data MUST NOT be given for Consumer zones");
1935 for (auto& resourceRecord
: new_records
) {
1936 resourceRecord
.qname
.makeUsLowerCase();
1937 if (!resourceRecord
.qname
.isPartOf(zonename
) && resourceRecord
.qname
!= zonename
) {
1938 throw ApiException("RRset " + resourceRecord
.qname
.toString() + " IN " + resourceRecord
.qtype
.toString() + ": Name is out of zone");
1941 apiCheckQNameAllowedCharacters(resourceRecord
.qname
.toString());
1943 if (resourceRecord
.qtype
.getCode() == QType::SOA
&& resourceRecord
.qname
== zonename
) {
1946 if (resourceRecord
.qtype
.getCode() == QType::NS
&& resourceRecord
.qname
== zonename
) {
1947 have_zone_ns
= true;
1951 // synthesize RRs as needed
1952 DNSResourceRecord autorr
;
1953 autorr
.qname
= zonename
;
1955 autorr
.ttl
= ::arg().asNum("default-ttl");
1957 if (!have_soa
&& zonekind
!= DomainInfo::Secondary
&& zonekind
!= DomainInfo::Consumer
) {
1958 // synthesize a SOA record so the zone "really" exists
1959 string soa
= ::arg()["default-soa-content"];
1960 boost::replace_all(soa
, "@", zonename
.toStringNoDot());
1962 fillSOAData(soa
, soaData
);
1963 soaData
.serial
= document
["serial"].int_value();
1964 autorr
.qtype
= QType::SOA
;
1965 autorr
.content
= makeSOAContent(soaData
)->getZoneRepresentation(true);
1966 // updateDomainSettingsFromDocument will apply SOA-EDIT-API as needed
1967 new_records
.push_back(autorr
);
1970 // create NS records if nameservers are given
1971 for (const auto& value
: nameservers
.array_items()) {
1972 const string
& nameserver
= value
.string_value();
1973 if (nameserver
.empty()) {
1974 throw ApiException("Nameservers must be non-empty strings");
1976 if (zonekind
== DomainInfo::Consumer
) {
1977 throw ApiException("Nameservers MUST NOT be given for Consumer zones");
1979 if (!isCanonical(nameserver
)) {
1980 throw ApiException("Nameserver is not canonical: '" + nameserver
+ "'");
1983 // ensure the name parses
1984 autorr
.content
= DNSName(nameserver
).toStringRootDot();
1987 throw ApiException("Unable to parse DNS Name for NS '" + nameserver
+ "'");
1989 autorr
.qtype
= QType::NS
;
1990 new_records
.push_back(autorr
);
1992 throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
1996 checkNewRecords(new_records
, zonename
);
1998 if (boolFromJson(document
, "dnssec", false)) {
1999 checkDefaultDNSSECAlgos();
2001 if (document
["nsec3param"].string_value().length() > 0) {
2002 NSEC3PARAMRecordContent
ns3pr(document
["nsec3param"].string_value());
2004 if (!dnssecKeeper
.checkNSEC3PARAM(ns3pr
, error_msg
)) {
2005 throw ApiException("NSEC3PARAMs provided for zone '" + zonename
.toString() + "' are invalid. " + error_msg
);
2010 // no going back after this
2011 if (!backend
.createDomain(zonename
, kind
.get_value_or(DomainInfo::Native
), primaries
.get_value_or(vector
<ComboAddress
>()), account
.get_value_or(""))) {
2012 throw ApiException("Creating domain '" + zonename
.toString() + "' failed: backend refused");
2015 if (!backend
.getDomainInfo(zonename
, domainInfo
)) {
2016 throw ApiException("Creating domain '" + zonename
.toString() + "' failed: lookup of domain ID failed");
2019 domainInfo
.backend
->startTransaction(zonename
, static_cast<int>(domainInfo
.id
));
2021 // will be overridden by updateDomainSettingsFromDocument, if given in document.
2022 domainInfo
.backend
->setDomainMetadataOne(zonename
, "SOA-EDIT-API", "DEFAULT");
2024 for (auto& resourceRecord
: new_records
) {
2025 resourceRecord
.domain_id
= static_cast<int>(domainInfo
.id
);
2026 domainInfo
.backend
->feedRecord(resourceRecord
, DNSName());
2028 for (Comment
& comment
: new_comments
) {
2029 comment
.domain_id
= static_cast<int>(domainInfo
.id
);
2030 if (!domainInfo
.backend
->feedComment(comment
)) {
2031 throw ApiException("Hosting backend does not support editing comments.");
2035 updateDomainSettingsFromDocument(backend
, domainInfo
, zonename
, document
, !new_records
.empty());
2037 if (!catalog
&& kind
== DomainInfo::Primary
) {
2038 const auto& defaultCatalog
= ::arg()["default-catalog-zone"];
2039 if (!defaultCatalog
.empty()) {
2040 domainInfo
.backend
->setCatalog(zonename
, DNSName(defaultCatalog
));
2044 domainInfo
.backend
->commitTransaction();
2046 g_zoneCache
.add(zonename
, static_cast<int>(domainInfo
.id
)); // make new zone visible
2048 fillZone(backend
, zonename
, resp
, req
);
2053 static void apiServerZonesGET(HttpRequest
* req
, HttpResponse
* resp
)
2055 UeberBackend backend
;
2056 DNSSECKeeper
dnssecKeeper(&backend
);
2057 vector
<DomainInfo
> domains
;
2059 if (req
->getvars
.count("zone") != 0) {
2060 string zone
= req
->getvars
["zone"];
2061 apiCheckNameAllowedCharacters(zone
);
2062 DNSName zonename
= apiNameToDNSName(zone
);
2063 zonename
.makeUsLowerCase();
2064 DomainInfo domainInfo
;
2065 if (backend
.getDomainInfo(zonename
, domainInfo
)) {
2066 domains
.push_back(domainInfo
);
2071 backend
.getAllDomains(&domains
, true, true); // incl. serial and disabled
2073 catch (const PDNSException
& exception
) {
2074 throw HttpInternalServerErrorException("Could not retrieve all domain information: " + exception
.reason
);
2078 bool with_dnssec
= true;
2079 if (req
->getvars
.count("dnssec") != 0) {
2080 // can send ?dnssec=false to improve performance.
2081 string dnssec_flag
= req
->getvars
["dnssec"];
2082 if (dnssec_flag
== "false") {
2083 with_dnssec
= false;
2088 doc
.reserve(domains
.size());
2089 for (const DomainInfo
& domainInfo
: domains
) {
2090 doc
.emplace_back(getZoneInfo(domainInfo
, with_dnssec
? &dnssecKeeper
: nullptr));
2092 resp
->setJsonBody(doc
);
2095 static void apiServerZoneDetailPUT(HttpRequest
* req
, HttpResponse
* resp
)
2097 ZoneData zoneData
{req
};
2099 // update domain contents and/or settings
2100 const auto& document
= req
->json();
2102 auto rrsets
= document
["rrsets"];
2103 bool zoneWasModified
= false;
2104 DomainInfo::DomainKind newKind
= zoneData
.domainInfo
.kind
;
2105 if (document
["kind"].is_string()) {
2106 newKind
= DomainInfo::stringToKind(stringFromJson(document
, "kind"));
2109 // if records/comments are given, load, check and insert them
2110 if (rrsets
.is_array()) {
2111 zoneWasModified
= true;
2112 bool haveSoa
= false;
2113 string soaEditApiKind
;
2115 zoneData
.domainInfo
.backend
->getDomainMetadataOne(zoneData
.zoneName
, "SOA-EDIT-API", soaEditApiKind
);
2116 zoneData
.domainInfo
.backend
->getDomainMetadataOne(zoneData
.zoneName
, "SOA-EDIT", soaEditKind
);
2118 vector
<DNSResourceRecord
> new_records
;
2119 vector
<Comment
> new_comments
;
2122 for (const auto& rrset
: rrsets
.array_items()) {
2123 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
2124 apiCheckQNameAllowedCharacters(qname
.toString());
2126 qtype
= stringFromJson(rrset
, "type");
2127 if (qtype
.getCode() == 0) {
2128 throw ApiException("RRset " + qname
.toString() + " IN " + stringFromJson(rrset
, "type") + ": unknown type given");
2130 if (rrset
["records"].is_array()) {
2131 uint32_t ttl
= uintFromJson(rrset
, "ttl");
2132 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
);
2134 if (rrset
["comments"].is_array()) {
2135 gatherComments(rrset
, qname
, qtype
, new_comments
);
2139 catch (const JsonException
& exc
) {
2140 throw ApiException("New RRsets are invalid: " + string(exc
.what()));
2143 for (auto& resourceRecord
: new_records
) {
2144 resourceRecord
.qname
.makeUsLowerCase();
2145 if (!resourceRecord
.qname
.isPartOf(zoneData
.zoneName
) && resourceRecord
.qname
!= zoneData
.zoneName
) {
2146 throw ApiException("RRset " + resourceRecord
.qname
.toString() + " IN " + resourceRecord
.qtype
.toString() + ": Name is out of zone");
2148 apiCheckQNameAllowedCharacters(resourceRecord
.qname
.toString());
2150 if (resourceRecord
.qtype
.getCode() == QType::SOA
&& resourceRecord
.qname
== zoneData
.zoneName
) {
2155 if (!haveSoa
&& newKind
!= DomainInfo::Secondary
&& newKind
!= DomainInfo::Consumer
) {
2156 // Require SOA if this is a primary zone.
2157 throw ApiException("Must give SOA record for zone when replacing all RR sets");
2159 if (newKind
== DomainInfo::Consumer
&& !new_records
.empty()) {
2160 // Allow deleting all RRsets, just not modifying them.
2161 throw ApiException("Modifying RRsets in Consumer zones is unsupported");
2164 checkNewRecords(new_records
, zoneData
.zoneName
);
2166 zoneData
.domainInfo
.backend
->startTransaction(zoneData
.zoneName
, static_cast<int>(zoneData
.domainInfo
.id
));
2167 for (auto& resourceRecord
: new_records
) {
2168 resourceRecord
.domain_id
= static_cast<int>(zoneData
.domainInfo
.id
);
2169 zoneData
.domainInfo
.backend
->feedRecord(resourceRecord
, DNSName());
2171 for (Comment
& comment
: new_comments
) {
2172 comment
.domain_id
= static_cast<int>(zoneData
.domainInfo
.id
);
2173 zoneData
.domainInfo
.backend
->feedComment(comment
);
2176 if (!haveSoa
&& (newKind
== DomainInfo::Secondary
|| newKind
== DomainInfo::Consumer
)) {
2177 zoneData
.domainInfo
.backend
->setStale(zoneData
.domainInfo
.id
);
2181 // avoid deleting current zone contents
2182 zoneData
.domainInfo
.backend
->startTransaction(zoneData
.zoneName
, -1);
2185 // updateDomainSettingsFromDocument will rectify the zone and update SOA serial.
2186 updateDomainSettingsFromDocument(zoneData
.backend
, zoneData
.domainInfo
, zoneData
.zoneName
, document
, zoneWasModified
);
2187 zoneData
.domainInfo
.backend
->commitTransaction();
2189 purgeAuthCaches(zoneData
.zoneName
.toString() + "$");
2192 resp
->status
= 204; // No Content, but indicate success
2195 static void apiServerZoneDetailDELETE(HttpRequest
* req
, HttpResponse
* resp
)
2197 ZoneData zoneData
{req
};
2201 zoneData
.domainInfo
.backend
->startTransaction(zoneData
.zoneName
, -1);
2203 if (!zoneData
.domainInfo
.backend
->deleteDomain(zoneData
.zoneName
)) {
2204 throw ApiException("Deleting domain '" + zoneData
.zoneName
.toString() + "' failed: backend delete failed/unsupported");
2207 zoneData
.domainInfo
.backend
->commitTransaction();
2209 g_zoneCache
.remove(zoneData
.zoneName
);
2212 zoneData
.domainInfo
.backend
->abortTransaction();
2217 DNSSECKeeper::clearCaches(zoneData
.zoneName
);
2218 purgeAuthCaches(zoneData
.zoneName
.toString() + "$");
2220 // empty body on success
2222 resp
->status
= 204; // No Content: declare that the zone is gone now
2225 static void apiServerZoneDetailPATCH(HttpRequest
* req
, HttpResponse
* resp
)
2227 ZoneData zoneData
{req
};
2228 patchZone(zoneData
.backend
, zoneData
.zoneName
, zoneData
.domainInfo
, req
, resp
);
2231 static void apiServerZoneDetailGET(HttpRequest
* req
, HttpResponse
* resp
)
2233 ZoneData zoneData
{req
};
2234 fillZone(zoneData
.backend
, zoneData
.zoneName
, resp
, req
);
2237 static void apiServerZoneExport(HttpRequest
* req
, HttpResponse
* resp
)
2239 ZoneData zoneData
{req
};
2241 ostringstream outputStringStream
;
2243 DNSResourceRecord resourceRecord
;
2245 zoneData
.domainInfo
.backend
->list(zoneData
.zoneName
, static_cast<int>(zoneData
.domainInfo
.id
));
2246 while (zoneData
.domainInfo
.backend
->get(resourceRecord
)) {
2247 if (resourceRecord
.qtype
.getCode() == 0) {
2248 continue; // skip empty non-terminals
2251 outputStringStream
<< resourceRecord
.qname
.toString() << "\t" << resourceRecord
.ttl
<< "\t"
2253 << "\t" << resourceRecord
.qtype
.toString() << "\t" << makeApiRecordContent(resourceRecord
.qtype
, resourceRecord
.content
) << endl
;
2256 if (req
->accept_json
) {
2257 resp
->setJsonBody(Json::object
{{"zone", outputStringStream
.str()}});
2260 resp
->headers
["Content-Type"] = "text/plain; charset=us-ascii";
2261 resp
->body
= outputStringStream
.str();
2265 static void apiServerZoneAxfrRetrieve(HttpRequest
* req
, HttpResponse
* resp
)
2267 ZoneData zoneData
{req
};
2269 if (zoneData
.domainInfo
.primaries
.empty()) {
2270 throw ApiException("Domain '" + zoneData
.zoneName
.toString() + "' is not a secondary domain (or has no primary defined)");
2273 shuffle(zoneData
.domainInfo
.primaries
.begin(), zoneData
.domainInfo
.primaries
.end(), pdns::dns_random_engine());
2274 Communicator
.addSuckRequest(zoneData
.zoneName
, zoneData
.domainInfo
.primaries
.front(), SuckRequest::Api
);
2275 resp
->setSuccessResult("Added retrieval request for '" + zoneData
.zoneName
.toString() + "' from primary " + zoneData
.domainInfo
.primaries
.front().toLogString());
2278 static void apiServerZoneNotify(HttpRequest
* req
, HttpResponse
* resp
)
2280 ZoneData zoneData
{req
};
2282 if (!Communicator
.notifyDomain(zoneData
.zoneName
, &zoneData
.backend
)) {
2283 throw ApiException("Failed to add to the queue - see server log");
2286 resp
->setSuccessResult("Notification queued");
2289 static void apiServerZoneRectify(HttpRequest
* req
, HttpResponse
* resp
)
2291 ZoneData zoneData
{req
};
2293 if (zoneData
.dnssecKeeper
.isPresigned(zoneData
.zoneName
)) {
2294 throw ApiException("Zone '" + zoneData
.zoneName
.toString() + "' is pre-signed, not rectifying.");
2299 if (!zoneData
.dnssecKeeper
.rectifyZone(zoneData
.zoneName
, error_msg
, info
, true)) {
2300 throw ApiException("Failed to rectify '" + zoneData
.zoneName
.toString() + "' " + error_msg
);
2303 resp
->setSuccessResult("Rectified");
2306 // NOLINTNEXTLINE(readability-function-cognitive-complexity): TODO Refactor this function.
2307 static void patchZone(UeberBackend
& backend
, const DNSName
& zonename
, DomainInfo
& domainInfo
, HttpRequest
* req
, HttpResponse
* resp
)
2309 bool zone_disabled
= false;
2312 vector
<DNSResourceRecord
> new_records
;
2313 vector
<Comment
> new_comments
;
2314 vector
<DNSResourceRecord
> new_ptrs
;
2316 Json document
= req
->json();
2318 auto rrsets
= document
["rrsets"];
2319 if (!rrsets
.is_array()) {
2320 throw ApiException("No rrsets given in update request");
2323 domainInfo
.backend
->startTransaction(zonename
);
2326 string soa_edit_api_kind
;
2327 string soa_edit_kind
;
2328 domainInfo
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT-API", soa_edit_api_kind
);
2329 domainInfo
.backend
->getDomainMetadataOne(zonename
, "SOA-EDIT", soa_edit_kind
);
2330 bool soa_edit_done
= false;
2332 set
<std::tuple
<DNSName
, QType
, string
>> seen
;
2334 for (const auto& rrset
: rrsets
.array_items()) {
2335 string changetype
= toUpper(stringFromJson(rrset
, "changetype"));
2336 DNSName qname
= apiNameToDNSName(stringFromJson(rrset
, "name"));
2337 apiCheckQNameAllowedCharacters(qname
.toString());
2339 qtype
= stringFromJson(rrset
, "type");
2340 if (qtype
.getCode() == 0) {
2341 throw ApiException("RRset " + qname
.toString() + " IN " + stringFromJson(rrset
, "type") + ": unknown type given");
2344 if (seen
.count({qname
, qtype
, changetype
}) != 0) {
2345 throw ApiException("Duplicate RRset " + qname
.toString() + " IN " + qtype
.toString() + " with changetype: " + changetype
);
2347 seen
.insert({qname
, qtype
, changetype
});
2349 if (changetype
== "DELETE") {
2350 // delete all matching qname/qtype RRs (and, implicitly comments).
2351 if (!domainInfo
.backend
->replaceRRSet(domainInfo
.id
, qname
, qtype
, vector
<DNSResourceRecord
>())) {
2352 throw ApiException("Hosting backend does not support editing records.");
2355 else if (changetype
== "REPLACE") {
2356 // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
2357 if (!qname
.isPartOf(zonename
) && qname
!= zonename
) {
2358 throw ApiException("RRset " + qname
.toString() + " IN " + qtype
.toString() + ": Name is out of zone");
2361 bool replace_records
= rrset
["records"].is_array();
2362 bool replace_comments
= rrset
["comments"].is_array();
2364 if (!replace_records
&& !replace_comments
) {
2365 throw ApiException("No change for RRset " + qname
.toString() + " IN " + qtype
.toString());
2368 new_records
.clear();
2369 new_comments
.clear();
2372 if (replace_records
) {
2373 // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2374 uint32_t ttl
= uintFromJson(rrset
, "ttl");
2375 gatherRecords(rrset
, qname
, qtype
, ttl
, new_records
);
2377 for (DNSResourceRecord
& resourceRecord
: new_records
) {
2378 resourceRecord
.domain_id
= static_cast<int>(domainInfo
.id
);
2379 if (resourceRecord
.qtype
.getCode() == QType::SOA
&& resourceRecord
.qname
== zonename
) {
2380 soa_edit_done
= increaseSOARecord(resourceRecord
, soa_edit_api_kind
, soa_edit_kind
);
2383 checkNewRecords(new_records
, zonename
);
2386 if (replace_comments
) {
2387 gatherComments(rrset
, qname
, qtype
, new_comments
);
2389 for (Comment
& comment
: new_comments
) {
2390 comment
.domain_id
= static_cast<int>(domainInfo
.id
);
2394 catch (const JsonException
& e
) {
2395 throw ApiException("New RRsets are invalid: " + string(e
.what()));
2398 if (replace_records
) {
2399 bool ent_present
= false;
2400 bool dname_seen
= false;
2401 bool ns_seen
= false;
2403 domainInfo
.backend
->lookup(QType(QType::ANY
), qname
, static_cast<int>(domainInfo
.id
));
2404 DNSResourceRecord resourceRecord
;
2405 while (domainInfo
.backend
->get(resourceRecord
)) {
2406 if (resourceRecord
.qtype
.getCode() == QType::ENT
) {
2408 /* that's fine, we will override it */
2411 if (qtype
== QType::DNAME
|| resourceRecord
.qtype
== QType::DNAME
) {
2414 if (qtype
== QType::NS
|| resourceRecord
.qtype
== QType::NS
) {
2417 if (qtype
.getCode() != resourceRecord
.qtype
.getCode()
2418 && (exclusiveEntryTypes
.count(qtype
.getCode()) != 0
2419 || exclusiveEntryTypes
.count(resourceRecord
.qtype
.getCode()) != 0)) {
2421 // leave database handle in a consistent state
2422 while (domainInfo
.backend
->get(resourceRecord
)) {
2426 throw ApiException("RRset " + qname
.toString() + " IN " + qtype
.toString() + ": Conflicts with pre-existing RRset");
2430 if (dname_seen
&& ns_seen
&& qname
!= zonename
) {
2431 throw ApiException("RRset " + qname
.toString() + " IN " + qtype
.toString() + ": Cannot have both NS and DNAME except in zone apex");
2433 if (!new_records
.empty() && domainInfo
.kind
== DomainInfo::Consumer
) {
2434 // Allow deleting all RRsets, just not modifying them.
2435 throw ApiException("Modifying RRsets in Consumer zones is unsupported");
2437 if (!new_records
.empty() && ent_present
) {
2439 if (!domainInfo
.backend
->replaceRRSet(domainInfo
.id
, qname
, qt_ent
, new_records
)) {
2440 throw ApiException("Hosting backend does not support editing records.");
2443 if (!domainInfo
.backend
->replaceRRSet(domainInfo
.id
, qname
, qtype
, new_records
)) {
2444 throw ApiException("Hosting backend does not support editing records.");
2447 if (replace_comments
) {
2448 if (!domainInfo
.backend
->replaceComments(domainInfo
.id
, qname
, qtype
, new_comments
)) {
2449 throw ApiException("Hosting backend does not support editing comments.");
2454 throw ApiException("Changetype not understood");
2458 zone_disabled
= (!backend
.getSOAUncached(zonename
, soaData
));
2460 // edit SOA (if needed)
2461 if (!zone_disabled
&& !soa_edit_api_kind
.empty() && !soa_edit_done
) {
2462 DNSResourceRecord resourceRecord
;
2463 if (makeIncreasedSOARecord(soaData
, soa_edit_api_kind
, soa_edit_kind
, resourceRecord
)) {
2464 if (!domainInfo
.backend
->replaceRRSet(domainInfo
.id
, resourceRecord
.qname
, resourceRecord
.qtype
, vector
<DNSResourceRecord
>(1, resourceRecord
))) {
2465 throw ApiException("Hosting backend does not support editing records.");
2469 // return old and new serials in headers
2470 resp
->headers
["X-PDNS-Old-Serial"] = std::to_string(soaData
.serial
);
2471 fillSOAData(resourceRecord
.content
, soaData
);
2472 resp
->headers
["X-PDNS-New-Serial"] = std::to_string(soaData
.serial
);
2476 domainInfo
.backend
->abortTransaction();
2481 DNSSECKeeper
dnssecKeeper(&backend
);
2482 if (!zone_disabled
&& !dnssecKeeper
.isPresigned(zonename
) && isZoneApiRectifyEnabled(domainInfo
)) {
2485 if (!dnssecKeeper
.rectifyZone(zonename
, error_msg
, info
, false)) {
2486 throw ApiException("Failed to rectify '" + zonename
.toString() + "' " + error_msg
);
2490 domainInfo
.backend
->commitTransaction();
2492 DNSSECKeeper::clearCaches(zonename
);
2493 purgeAuthCaches(zonename
.toString() + "$");
2496 resp
->status
= 204; // No Content, but indicate success
2499 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
)
2501 string qVar
= req
->getvars
["q"];
2502 string sMaxVar
= req
->getvars
["max"];
2503 string sObjectTypeVar
= req
->getvars
["object_type"];
2505 size_t maxEnts
= 100;
2508 // the following types of data can be searched for using the api
2509 enum class ObjectType
2518 throw ApiException("Query q can't be blank");
2520 if (!sMaxVar
.empty()) {
2521 maxEnts
= std::stoi(sMaxVar
);
2524 throw ApiException("Maximum entries must be larger than 0");
2527 if (sObjectTypeVar
.empty() || sObjectTypeVar
== "all") {
2528 objectType
= ObjectType::ALL
;
2530 else if (sObjectTypeVar
== "zone") {
2531 objectType
= ObjectType::ZONE
;
2533 else if (sObjectTypeVar
== "record") {
2534 objectType
= ObjectType::RECORD
;
2536 else if (sObjectTypeVar
== "comment") {
2537 objectType
= ObjectType::COMMENT
;
2540 throw ApiException("object_type must be one of the following options: all, zone, record, comment");
2543 SimpleMatch
simpleMatch(qVar
, true);
2544 UeberBackend backend
;
2545 vector
<DomainInfo
> domains
;
2546 vector
<DNSResourceRecord
> result_rr
;
2547 vector
<Comment
> result_c
;
2548 map
<int, DomainInfo
> zoneIdZone
;
2549 map
<int, DomainInfo
>::iterator val
;
2552 backend
.getAllDomains(&domains
, false, true);
2554 for (const DomainInfo
& domainInfo
: domains
) {
2555 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::ZONE
) && ents
< maxEnts
&& simpleMatch
.match(domainInfo
.zone
)) {
2556 doc
.push_back(Json::object
{
2557 {"object_type", "zone"},
2558 {"zone_id", apiZoneNameToId(domainInfo
.zone
)},
2559 {"name", domainInfo
.zone
.toString()}});
2562 zoneIdZone
[static_cast<int>(domainInfo
.id
)] = domainInfo
; // populate cache
2565 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::RECORD
) && backend
.searchRecords(qVar
, maxEnts
, result_rr
)) {
2566 for (const DNSResourceRecord
& resourceRecord
: result_rr
) {
2567 if (resourceRecord
.qtype
.getCode() == 0) {
2568 continue; // skip empty non-terminals
2571 auto object
= Json::object
{
2572 {"object_type", "record"},
2573 {"name", resourceRecord
.qname
.toString()},
2574 {"type", resourceRecord
.qtype
.toString()},
2575 {"ttl", (double)resourceRecord
.ttl
},
2576 {"disabled", resourceRecord
.disabled
},
2577 {"content", makeApiRecordContent(resourceRecord
.qtype
, resourceRecord
.content
)}};
2579 val
= zoneIdZone
.find(resourceRecord
.domain_id
);
2580 if (val
!= zoneIdZone
.end()) {
2581 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2582 object
["zone"] = val
->second
.zone
.toString();
2584 doc
.emplace_back(object
);
2588 if ((objectType
== ObjectType::ALL
|| objectType
== ObjectType::COMMENT
) && backend
.searchComments(qVar
, maxEnts
, result_c
)) {
2589 for (const Comment
& comment
: result_c
) {
2590 auto object
= Json::object
{
2591 {"object_type", "comment"},
2592 {"name", comment
.qname
.toString()},
2593 {"type", comment
.qtype
.toString()},
2594 {"content", comment
.content
}};
2596 val
= zoneIdZone
.find(comment
.domain_id
);
2597 if (val
!= zoneIdZone
.end()) {
2598 object
["zone_id"] = apiZoneNameToId(val
->second
.zone
);
2599 object
["zone"] = val
->second
.zone
.toString();
2601 doc
.emplace_back(object
);
2605 resp
->setJsonBody(doc
);
2608 static void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
)
2610 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
2612 if (g_zoneCache
.isEnabled()) {
2613 DomainInfo domainInfo
;
2614 UeberBackend backend
;
2615 if (backend
.getDomainInfo(canon
, domainInfo
, false)) {
2616 // zone exists (uncached), add/update it in the zone cache.
2617 // Handle this first, to avoid concurrent queries re-populating the other caches.
2618 g_zoneCache
.add(domainInfo
.zone
, static_cast<int>(domainInfo
.id
));
2621 g_zoneCache
.remove(domainInfo
.zone
);
2625 DNSSECKeeper::clearCaches(canon
);
2626 // purge entire zone from cache, not just zone-level records.
2627 uint64_t count
= purgeAuthCaches(canon
.toString() + "$");
2628 resp
->setJsonBody(Json::object
{
2629 {"count", (int)count
},
2630 {"result", "Flushed cache."}});
2633 static std::ostream
& operator<<(std::ostream
& outStream
, StatType statType
)
2636 case StatType::counter
:
2637 return outStream
<< "counter";
2638 case StatType::gauge
:
2639 return outStream
<< "gauge";
2641 return outStream
<< static_cast<uint16_t>(statType
);
2644 static void prometheusMetrics(HttpRequest
* /* req */, HttpResponse
* resp
)
2646 std::ostringstream output
;
2647 for (const auto& metricName
: S
.getEntries()) {
2648 // Prometheus suggest using '_' instead of '-'
2649 std::string prometheusMetricName
= "pdns_auth_" + boost::replace_all_copy(metricName
, "-", "_");
2651 output
<< "# HELP " << prometheusMetricName
<< " " << S
.getDescrip(metricName
) << "\n";
2652 output
<< "# TYPE " << prometheusMetricName
<< " " << S
.getStatType(metricName
) << "\n";
2653 output
<< prometheusMetricName
<< " " << S
.read(metricName
) << "\n";
2656 output
<< "# HELP pdns_auth_info "
2657 << "Info from PowerDNS, value is always 1"
2659 output
<< "# TYPE pdns_auth_info "
2662 output
<< "pdns_auth_info{version=\"" << VERSION
<< "\"} "
2666 resp
->body
= output
.str();
2667 resp
->headers
["Content-Type"] = "text/plain";
2671 static void cssfunction(HttpRequest
* /* req */, HttpResponse
* resp
)
2673 resp
->headers
["Cache-Control"] = "max-age=86400";
2674 resp
->headers
["Content-Type"] = "text/css";
2677 ret
<< "* { box-sizing: border-box; margin: 0; padding: 0; }" << endl
;
2678 ret
<< "body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }" << endl
;
2679 ret
<< "a { color: #0959c2; }" << endl
;
2680 ret
<< "a:hover { color: #3B8EC8; }" << endl
;
2681 ret
<< ".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }" << endl
;
2682 ret
<< ".row:before, .row:after { display: table; content:\" \"; }" << endl
;
2683 ret
<< ".row:after { clear: both; }" << endl
;
2684 ret
<< ".columns { position: relative; min-height: 1px; float: left; }" << endl
;
2685 ret
<< ".all { width: 100%; }" << endl
;
2686 ret
<< ".headl { width: 60%; }" << endl
;
2687 ret
<< ".header { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
2688 ret
<< "background-image: url(data:image/png;base64,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);";
2689 ret
<< " width: 154px; height: 20px; }" << endl
;
2690 ret
<< "a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }" << endl
;
2691 ret
<< "footer { border-top: 1px solid #ddd; padding-top: 4px; font-size: 12px; }" << endl
;
2692 ret
<< "footer.row { margin-top: 1em; margin-bottom: 1em; }" << endl
;
2693 ret
<< ".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }" << endl
;
2694 ret
<< "table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }" << endl
;
2695 ret
<< "table.data td { border-bottom: 1px solid #333; padding: 2px; }" << endl
;
2696 ret
<< "table.data tr:nth-child(2n) { background: #e2e2e2; }" << endl
;
2697 ret
<< "table.data tr:hover { background: white; }" << endl
;
2698 ret
<< ".ringmeta { margin-bottom: 5px; }" << endl
;
2699 ret
<< ".resetring {float: right; }" << endl
;
2700 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
;
2701 ret
<< ".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}" << endl
;
2702 ret
<< ".resizering {float: right;}" << endl
;
2703 resp
->body
= ret
.str();
2707 void AuthWebServer::webThread()
2710 setThreadName("pdns/webserver");
2711 if (::arg().mustDo("api")) {
2712 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", apiServerCacheFlush
, "PUT");
2713 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", apiServerConfig
, "GET");
2714 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", apiServerSearchData
, "GET");
2715 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", apiServerStatistics
, "GET");
2716 d_ws
->registerApiHandler("/api/v1/servers/localhost/autoprimaries/<ip>/<nameserver>", &apiServerAutoprimaryDetailDELETE
, "DELETE");
2717 d_ws
->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesGET
, "GET");
2718 d_ws
->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesPOST
, "POST");
2719 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailGET
, "GET");
2720 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailPUT
, "PUT");
2721 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailDELETE
, "DELETE");
2722 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysGET
, "GET");
2723 d_ws
->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysPOST
, "POST");
2724 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", apiServerZoneAxfrRetrieve
, "PUT");
2725 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysGET
, "GET");
2726 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysPOST
, "POST");
2727 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysPUT
, "PUT");
2728 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysDELETE
, "DELETE");
2729 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeysGET
, "GET");
2730 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeysPOST
, "POST");
2731 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", apiServerZoneExport
, "GET");
2732 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindGET
, "GET");
2733 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindPUT
, "PUT");
2734 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindDELETE
, "DELETE");
2735 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadataGET
, "GET");
2736 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadataPOST
, "POST");
2737 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", apiServerZoneNotify
, "PUT");
2738 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", apiServerZoneRectify
, "PUT");
2739 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailGET
, "GET");
2740 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPATCH
, "PATCH");
2741 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPUT
, "PUT");
2742 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailDELETE
, "DELETE");
2743 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesGET
, "GET");
2744 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesPOST
, "POST");
2745 d_ws
->registerApiHandler("/api/v1/servers/localhost", apiServerDetail
, "GET");
2746 d_ws
->registerApiHandler("/api/v1/servers", apiServer
, "GET");
2747 d_ws
->registerApiHandler("/api/v1", apiDiscoveryV1
, "GET");
2748 d_ws
->registerApiHandler("/api/docs", apiDocs
, "GET");
2749 d_ws
->registerApiHandler("/api", apiDiscovery
, "GET");
2751 if (::arg().mustDo("webserver")) {
2752 d_ws
->registerWebHandler(
2753 "/style.css", [](HttpRequest
* req
, HttpResponse
* resp
) { cssfunction(req
, resp
); }, "GET");
2754 d_ws
->registerWebHandler(
2755 "/", [this](HttpRequest
* req
, HttpResponse
* resp
) { indexfunction(req
, resp
); }, "GET");
2756 d_ws
->registerWebHandler("/metrics", prometheusMetrics
, "GET");
2761 g_log
<< Logger::Error
<< "AuthWebServer thread caught an exception, dying" << endl
;