]>
Commit | Line | Data |
---|---|---|
12c86877 | 1 | /* |
6edbf68a PL |
2 | * This file is part of PowerDNS or dnsdist. |
3 | * Copyright -- PowerDNS.COM B.V. and its contributors | |
4 | * | |
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. | |
8 | * | |
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. | |
12 | * | |
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. | |
17 | * | |
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. | |
21 | */ | |
7330f904 FM |
22 | #include "dnsbackend.hh" |
23 | #include "webserver.hh" | |
ad528718 | 24 | #include <array> |
870a0fe4 AT |
25 | #ifdef HAVE_CONFIG_H |
26 | #include "config.h" | |
27 | #endif | |
9054d8a4 | 28 | #include "utility.hh" |
d267d1bf | 29 | #include "dynlistener.hh" |
2470b36e | 30 | #include "ws-auth.hh" |
e611a06c | 31 | #include "json.hh" |
12c86877 BH |
32 | #include "logger.hh" |
33 | #include "statbag.hh" | |
34 | #include "misc.hh" | |
6f16f97c | 35 | #include "base64.hh" |
12c86877 BH |
36 | #include "arguments.hh" |
37 | #include "dns.hh" | |
6cc98ddf | 38 | #include "comment.hh" |
e611a06c | 39 | #include "ueberbackend.hh" |
dcc65f25 | 40 | #include <boost/format.hpp> |
fa8fd4d2 | 41 | |
9ac4a7c6 | 42 | #include "namespaces.hh" |
6ec5e728 | 43 | #include "ws-api.hh" |
ba1a571d | 44 | #include "version.hh" |
d29d5db7 | 45 | #include "dnsseckeeper.hh" |
3c3c006b | 46 | #include <iomanip> |
0f0e73fe | 47 | #include "zoneparser-tng.hh" |
8cb70f23 | 48 | #include "auth-main.hh" |
bf269e28 | 49 | #include "auth-caches.hh" |
f4922e19 | 50 | #include "auth-zonecache.hh" |
519f5484 | 51 | #include "threadname.hh" |
ac5298aa | 52 | #include "tsigutils.hh" |
8537b9f0 | 53 | |
24afabad | 54 | using json11::Json; |
12c86877 | 55 | |
2c03bdc8 FM |
56 | Ewma::Ewma() { dt.set(); } |
57 | ||
58 | void Ewma::submit(int val) | |
59 | { | |
60 | int rate = val - d_last; | |
61 | double difft = dt.udiff() / 1000000.0; | |
62 | dt.set(); | |
63 | ||
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); | |
68 | ||
69 | d_last = val; | |
70 | } | |
71 | ||
72 | double Ewma::get10() const | |
73 | { | |
74 | return d_10; | |
75 | } | |
76 | ||
77 | double Ewma::get5() const | |
78 | { | |
79 | return d_5; | |
80 | } | |
81 | ||
82 | double Ewma::get1() const | |
83 | { | |
84 | return d_1; | |
85 | } | |
86 | ||
87 | double Ewma::getMax() const | |
88 | { | |
89 | return d_max; | |
90 | } | |
91 | ||
efc47b04 | 92 | static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp); |
f63168e6 | 93 | |
646bcd7d | 94 | // QTypes that MUST NOT have multiple records of the same type in a given RRset. |
212f57b8 | 95 | static const std::set<uint16_t> onlyOneEntryTypes = {QType::CNAME, QType::DNAME, QType::SOA}; |
646bcd7d | 96 | // QTypes that MUST NOT be used with any other QType on the same name. |
212f57b8 | 97 | static const std::set<uint16_t> exclusiveEntryTypes = {QType::CNAME}; |
2eb206ec | 98 | // QTypes that MUST be at apex. |
c9767646 | 99 | static const std::set<uint16_t> atApexTypes = {QType::SOA, QType::DNSKEY}; |
2eb206ec KM |
100 | // QTypes that are NOT allowed at apex. |
101 | static const std::set<uint16_t> nonApexTypes = {QType::DS}; | |
646bcd7d | 102 | |
8a70e507 | 103 | AuthWebServer::AuthWebServer() : |
eace2c24 | 104 | d_start(time(nullptr)), |
8a70e507 CHB |
105 | d_min10(0), |
106 | d_min5(0), | |
eace2c24 | 107 | d_min1(0) |
12c86877 | 108 | { |
64c4f83c | 109 | if (arg().mustDo("webserver") || arg().mustDo("api")) { |
2bbc9eb0 | 110 | d_ws = std::make_unique<WebServer>(arg()["webserver-address"], arg().asNum("webserver-port")); |
64c4f83c RG |
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")); | |
8ca656a8 | 113 | d_ws->setLogLevel(arg()["webserver-loglevel"]); |
0010aefa PL |
114 | |
115 | NetmaskGroup acl; | |
116 | acl.toMasks(::arg()["webserver-allow-from"]); | |
117 | d_ws->setACL(acl); | |
118 | ||
214b034e PD |
119 | d_ws->setMaxBodySize(::arg().asNum("webserver-max-bodysize")); |
120 | ||
825fa717 CH |
121 | d_ws->bind(); |
122 | } | |
12c86877 BH |
123 | } |
124 | ||
a690b08c | 125 | void AuthWebServer::go(StatBag& stats) |
12c86877 | 126 | { |
536ab56f | 127 | S.doRings(); |
212f57b8 | 128 | std::thread webT([this]() { webThread(); }); |
0ddde5fb | 129 | webT.detach(); |
a690b08c | 130 | std::thread statT([this, &stats]() { statThread(stats); }); |
0ddde5fb | 131 | statT.detach(); |
12c86877 BH |
132 | } |
133 | ||
a690b08c | 134 | void AuthWebServer::statThread(StatBag& stats) |
12c86877 BH |
135 | { |
136 | try { | |
519f5484 | 137 | setThreadName("pdns/statHelper"); |
212f57b8 | 138 | for (;;) { |
a690b08c FM |
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"))); | |
12c86877 BH |
144 | Utility::sleep(1); |
145 | } | |
146 | } | |
212f57b8 FM |
147 | catch (...) { |
148 | g_log << Logger::Error << "Webserver statThread caught an exception, dying" << endl; | |
5bd2ea7b | 149 | _exit(1); |
12c86877 BH |
150 | } |
151 | } | |
152 | ||
ad528718 | 153 | static string htmlescape(const string& inputString) |
212f57b8 | 154 | { |
9f3fdaa0 | 155 | string result; |
ad528718 FM |
156 | for (char currentChar : inputString) { |
157 | switch (currentChar) { | |
9f3fdaa0 | 158 | case '&': |
c86a96f9 | 159 | result += "&"; |
9f3fdaa0 CH |
160 | break; |
161 | case '<': | |
162 | result += "<"; | |
163 | break; | |
164 | case '>': | |
165 | result += ">"; | |
166 | break; | |
c7f59d62 PL |
167 | case '"': |
168 | result += """; | |
169 | break; | |
9f3fdaa0 | 170 | default: |
ad528718 | 171 | result += currentChar; |
9f3fdaa0 CH |
172 | } |
173 | } | |
174 | return result; | |
175 | } | |
176 | ||
212f57b8 | 177 | static void printtable(ostringstream& ret, const string& ringname, const string& title, int limit = 10) |
12c86877 | 178 | { |
ad528718 | 179 | unsigned int tot = 0; |
212f57b8 FM |
180 | int entries = 0; |
181 | vector<pair<string, unsigned int>> ring = S.getRing(ringname); | |
12c86877 | 182 | |
ad528718 FM |
183 | for (const auto& entry : ring) { |
184 | tot += entry.second; | |
12c86877 BH |
185 | entries++; |
186 | } | |
187 | ||
212f57b8 FM |
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: "; | |
ad528718 FM |
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]) { | |
212f57b8 | 197 | ret << "<a href=\"?resizering=" << htmlescape(ringname) << "&size=" << sizes[i] << "\">" << sizes[i] << "</a> "; |
ad528718 FM |
198 | } |
199 | else { | |
212f57b8 | 200 | ret << "(" << sizes[i] << ") "; |
ad528718 | 201 | } |
12c86877 | 202 | } |
212f57b8 | 203 | ret << "</span></div>"; |
12c86877 | 204 | |
212f57b8 | 205 | ret << "<table class=\"data\">"; |
ad528718 FM |
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) { | |
212f57b8 FM |
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; | |
12c86877 | 211 | } |
212f57b8 | 212 | ret << "<tr><td colspan=3></td></tr>" << endl; |
ad528718 | 213 | if (printed != tot) { |
212f57b8 | 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; |
ad528718 | 215 | } |
12c86877 | 216 | |
212f57b8 FM |
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; | |
12c86877 BH |
219 | } |
220 | ||
ad528718 | 221 | static void printvars(ostringstream& ret) |
12c86877 | 222 | { |
212f57b8 | 223 | ret << "<div class=panel><h2>Variables</h2><table class=\"data\">" << endl; |
12c86877 | 224 | |
212f57b8 FM |
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; | |
12c86877 | 228 | } |
e2a77e08 | 229 | |
212f57b8 | 230 | ret << "</table></div>" << endl; |
12c86877 BH |
231 | } |
232 | ||
ad528718 | 233 | static void printargs(ostringstream& ret) |
12c86877 | 234 | { |
212f57b8 | 235 | ret << R"(<table border=1><tr><td colspan=3 bgcolor="#0000ff"><font color="#ffffff">Arguments</font></td>)" << endl; |
12c86877 | 236 | |
212f57b8 FM |
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; | |
12c86877 BH |
240 | } |
241 | } | |
242 | ||
dea47634 | 243 | string AuthWebServer::makePercentage(const double& val) |
b6f57093 BH |
244 | { |
245 | return (boost::format("%.01f%%") % val).str(); | |
246 | } | |
247 | ||
dea47634 | 248 | void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp) |
12c86877 | 249 | { |
212f57b8 | 250 | if (!req->getvars["resetring"].empty()) { |
ad528718 | 251 | if (S.ringExists(req->getvars["resetring"])) { |
583ea80d | 252 | S.resetRing(req->getvars["resetring"]); |
ad528718 | 253 | } |
d7b8730e | 254 | resp->status = 302; |
0665b7e6 | 255 | resp->headers["Location"] = req->url.path; |
80d59cd1 | 256 | return; |
12c86877 | 257 | } |
212f57b8 FM |
258 | if (!req->getvars["resizering"].empty()) { |
259 | int size = std::stoi(req->getvars["size"]); | |
ad528718 | 260 | if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000) { |
335da0ba | 261 | S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"])); |
ad528718 | 262 | } |
d7b8730e | 263 | resp->status = 302; |
0665b7e6 | 264 | resp->headers["Location"] = req->url.path; |
80d59cd1 | 265 | return; |
12c86877 BH |
266 | } |
267 | ||
268 | ostringstream ret; | |
269 | ||
212f57b8 FM |
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; | |
275 | ||
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"]) << "]"; | |
281 | } | |
282 | ret << "</a></div>" << endl; | |
283 | ret << "<div class=\"header columns\"></div></div>"; | |
284 | ret << R"(<div class="row"><div class="all columns">)"; | |
285 | ||
286 | time_t passed = time(nullptr) - g_starttime; | |
287 | ||
288 | ret << "<p>Uptime: " << humanDuration(passed) << "<br>" << endl; | |
289 | ||
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; | |
291 | ||
ad528718 | 292 | if (d_cachemisses.get10() + d_cachehits.get10() > 0) { |
212f57b8 | 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; |
ad528718 | 294 | } |
212f57b8 | 295 | |
ad528718 | 296 | if (d_qcachemisses.get10() + d_qcachehits.get10() > 0) { |
212f57b8 | 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; |
ad528718 | 298 | } |
212f57b8 FM |
299 | |
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; | |
301 | ||
ad528718 | 302 | ret << "Total queries: " << S.read("udp-queries") << ". Question/answer latency: " << static_cast<double>(S.read("latency")) / 1000.0 << "ms</p><br>" << endl; |
212f57b8 | 303 | if (req->getvars["ring"].empty()) { |
eb029b8e | 304 | auto entries = S.listRings(); |
ad528718 FM |
305 | for (const auto& entry : entries) { |
306 | printtable(ret, entry, S.getRingTitle(entry)); | |
eb029b8e | 307 | } |
12c86877 | 308 | |
f6154a3b | 309 | printvars(ret); |
ad528718 | 310 | if (arg().mustDo("webserver-print-arguments")) { |
f6154a3b | 311 | printargs(ret); |
ad528718 | 312 | } |
12c86877 | 313 | } |
ad528718 | 314 | else if (S.ringExists(req->getvars["ring"])) { |
212f57b8 | 315 | printtable(ret, req->getvars["ring"], S.getRingTitle(req->getvars["ring"]), 100); |
ad528718 | 316 | } |
12c86877 | 317 | |
212f57b8 FM |
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; | |
12c86877 | 321 | |
80d59cd1 | 322 | resp->body = ret.str(); |
61f5d289 | 323 | resp->status = 200; |
12c86877 BH |
324 | } |
325 | ||
1d6b70f9 | 326 | /** Helper to build a record content as needed. */ |
212f57b8 FM |
327 | static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) |
328 | { | |
1d6b70f9 | 329 | // noDot: for backend storage, pass true. for API users, pass false. |
d525b58b | 330 | auto drc = DNSRecordContent::make(qtype.getCode(), QClass::IN, content); |
7fe1a82b | 331 | return drc->getZoneRepresentation(noDot); |
1d6b70f9 CH |
332 | } |
333 | ||
334 | /** "Normalize" record content for API consumers. */ | |
212f57b8 FM |
335 | static inline string makeApiRecordContent(const QType& qtype, const string& content) |
336 | { | |
1d6b70f9 CH |
337 | return makeRecordContent(qtype, content, false); |
338 | } | |
339 | ||
340 | /** "Normalize" record content for backend storage. */ | |
212f57b8 FM |
341 | static inline string makeBackendRecordContent(const QType& qtype, const string& content) |
342 | { | |
1d6b70f9 CH |
343 | return makeRecordContent(qtype, content, true); |
344 | } | |
345 | ||
ad528718 | 346 | static Json::object getZoneInfo(const DomainInfo& domainInfo, DNSSECKeeper* dnssecKeeper) |
212f57b8 | 347 | { |
ad528718 | 348 | string zoneId = apiZoneNameToId(domainInfo.zone); |
d525b58b | 349 | vector<string> primaries; |
ad528718 FM |
350 | primaries.reserve(domainInfo.primaries.size()); |
351 | for (const auto& primary : domainInfo.primaries) { | |
352 | primaries.push_back(primary.toStringWithPortExcept(53)); | |
c8b929d9 | 353 | } |
24ded6cc | 354 | |
a0930e45 | 355 | auto obj = Json::object{ |
62a9a74c | 356 | // id is the canonical lookup key, which doesn't actually match the name (in some cases) |
a0930e45 KM |
357 | {"id", zoneId}, |
358 | {"url", "/api/v1/servers/localhost/zones/" + zoneId}, | |
ad528718 FM |
359 | {"name", domainInfo.zone.toString()}, |
360 | {"kind", domainInfo.getKindString()}, | |
361 | {"catalog", (!domainInfo.catalog.empty() ? domainInfo.catalog.toString() : "")}, | |
362 | {"account", domainInfo.account}, | |
d525b58b | 363 | {"masters", std::move(primaries)}, |
ad528718 FM |
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); | |
cf0541a3 | 369 | string soa_edit; |
ad528718 FM |
370 | dnssecKeeper->getSoaEdit(domainInfo.zone, soa_edit, false); |
371 | obj["edited_serial"] = (double)calculateEditSOA(domainInfo.serial, soa_edit, domainInfo.zone); | |
07a32d18 CH |
372 | } |
373 | return obj; | |
c04b5870 CH |
374 | } |
375 | ||
212f57b8 FM |
376 | static bool shouldDoRRSets(HttpRequest* req) |
377 | { | |
ad528718 | 378 | if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true") { |
986e4858 | 379 | return true; |
ad528718 FM |
380 | } |
381 | if (req->getvars["rrsets"] == "false") { | |
986e4858 | 382 | return false; |
ad528718 FM |
383 | } |
384 | ||
212f57b8 | 385 | throw ApiException("'rrsets' request parameter value '" + req->getvars["rrsets"] + "' is not supported"); |
986e4858 PL |
386 | } |
387 | ||
ad528718 | 388 | static void fillZone(UeberBackend& backend, const DNSName& zonename, HttpResponse* resp, HttpRequest* req) |
212f57b8 | 389 | { |
ad528718 FM |
390 | DomainInfo domainInfo; |
391 | ||
392 | if (!backend.getDomainInfo(zonename, domainInfo)) { | |
77bfe8de PL |
393 | throw HttpNotFoundException(); |
394 | } | |
1abb81f4 | 395 | |
ad528718 FM |
396 | DNSSECKeeper dnssecKeeper(&backend); |
397 | Json::object doc = getZoneInfo(domainInfo, &dnssecKeeper); | |
62a9a74c | 398 | // extra stuff getZoneInfo doesn't do for us (more expensive) |
d29d5db7 | 399 | string soa_edit_api; |
ad528718 | 400 | domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api); |
62a9a74c | 401 | doc["soa_edit_api"] = soa_edit_api; |
6bb25159 | 402 | string soa_edit; |
ad528718 | 403 | domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit); |
62a9a74c | 404 | doc["soa_edit"] = soa_edit; |
efc52697 | 405 | |
986e4858 | 406 | string nsec3param; |
986e4858 | 407 | bool nsec3narrowbool = false; |
ad528718 | 408 | bool is_secured = dnssecKeeper.isSecuredZone(zonename); |
efc52697 | 409 | if (is_secured) { // ignore NSEC3PARAM and NSEC3NARROW metadata present in the db for unsigned zones |
ad528718 | 410 | domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param); |
efc52697 | 411 | string nsec3narrow; |
ad528718 | 412 | domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow); |
efc52697 KM |
413 | if (nsec3narrow == "1") { |
414 | nsec3narrowbool = true; | |
415 | } | |
416 | } | |
417 | doc["nsec3param"] = nsec3param; | |
986e4858 | 418 | doc["nsec3narrow"] = nsec3narrowbool; |
efc52697 | 419 | doc["dnssec"] = is_secured; |
986e4858 PL |
420 | |
421 | string api_rectify; | |
ad528718 | 422 | domainInfo.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify); |
986e4858 PL |
423 | doc["api_rectify"] = (api_rectify == "1"); |
424 | ||
eecc9ed5 | 425 | // TSIG |
ad528718 FM |
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); | |
eecc9ed5 | 430 | |
d525b58b KM |
431 | Json::array tsig_primary_keys; |
432 | for (const auto& keyname : tsig_primary) { | |
ad528718 | 433 | tsig_primary_keys.emplace_back(apiZoneNameToId(DNSName(keyname))); |
eecc9ed5 | 434 | } |
d525b58b | 435 | doc["master_tsig_key_ids"] = tsig_primary_keys; |
eecc9ed5 | 436 | |
c02c999b KM |
437 | Json::array tsig_secondary_keys; |
438 | for (const auto& keyname : tsig_secondary) { | |
ad528718 | 439 | tsig_secondary_keys.emplace_back(apiZoneNameToId(DNSName(keyname))); |
eecc9ed5 | 440 | } |
c02c999b | 441 | doc["slave_tsig_key_ids"] = tsig_secondary_keys; |
eecc9ed5 | 442 | |
de7fd3c6 | 443 | if (shouldDoRRSets(req)) { |
986e4858 PL |
444 | vector<DNSResourceRecord> records; |
445 | vector<Comment> comments; | |
446 | ||
447 | // load all records + sort | |
448 | { | |
ad528718 | 449 | DNSResourceRecord resourceRecord; |
486210b8 | 450 | if (req->getvars.count("rrset_name") == 0) { |
ad528718 | 451 | domainInfo.backend->list(zonename, static_cast<int>(domainInfo.id), true); // incl. disabled |
212f57b8 FM |
452 | } |
453 | else { | |
ad528718 | 454 | QType qType; |
486210b8 | 455 | if (req->getvars.count("rrset_type") == 0) { |
ad528718 | 456 | qType = QType::ANY; |
212f57b8 FM |
457 | } |
458 | else { | |
ad528718 | 459 | qType = req->getvars["rrset_type"]; |
486210b8 | 460 | } |
ad528718 | 461 | domainInfo.backend->lookup(qType, DNSName(req->getvars["rrset_name"]), static_cast<int>(domainInfo.id)); |
486210b8 | 462 | } |
ad528718 FM |
463 | while (domainInfo.backend->get(resourceRecord)) { |
464 | if (resourceRecord.qtype.getCode() == 0) { | |
986e4858 | 465 | continue; // skip empty non-terminals |
ad528718 FM |
466 | } |
467 | records.push_back(resourceRecord); | |
986e4858 | 468 | } |
ad528718 | 469 | sort(records.begin(), records.end(), [](const DNSResourceRecord& rrA, const DNSResourceRecord& rrB) { |
212f57b8 FM |
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 */ | |
ad528718 FM |
473 | if (rrA.qname == rrB.qname) { |
474 | return rrB.qtype < rrA.qtype; | |
212f57b8 | 475 | } |
ad528718 | 476 | return rrB.qname < rrA.qname; |
212f57b8 | 477 | }); |
6754ef71 | 478 | } |
6754ef71 | 479 | |
986e4858 PL |
480 | // load all comments + sort |
481 | { | |
482 | Comment comment; | |
ad528718 FM |
483 | domainInfo.backend->listComments(domainInfo.id); |
484 | while (domainInfo.backend->getComment(comment)) { | |
986e4858 PL |
485 | comments.push_back(comment); |
486 | } | |
ad528718 | 487 | sort(comments.begin(), comments.end(), [](const Comment& rrA, const Comment& rrB) { |
212f57b8 FM |
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 */ | |
ad528718 FM |
491 | if (rrA.qname == rrB.qname) { |
492 | return rrB.qtype < rrA.qtype; | |
212f57b8 | 493 | } |
ad528718 | 494 | return rrB.qname < rrA.qname; |
212f57b8 | 495 | }); |
6754ef71 | 496 | } |
6754ef71 | 497 | |
986e4858 PL |
498 | Json::array rrsets; |
499 | Json::object rrset; | |
500 | Json::array rrset_records; | |
501 | Json::array rrset_comments; | |
502 | DNSName current_qname; | |
503 | QType current_qtype; | |
ad528718 | 504 | uint32_t ttl = 0; |
986e4858 PL |
505 | auto rit = records.begin(); |
506 | auto cit = comments.begin(); | |
507 | ||
508 | while (rit != records.end() || cit != comments.end()) { | |
5481c77c PD |
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))) { | |
986e4858 PL |
511 | current_qname = rit->qname; |
512 | current_qtype = rit->qtype; | |
513 | ttl = rit->ttl; | |
212f57b8 FM |
514 | } |
515 | else { | |
986e4858 PL |
516 | current_qname = cit->qname; |
517 | current_qtype = cit->qtype; | |
518 | ttl = 0; | |
519 | } | |
6754ef71 | 520 | |
212f57b8 | 521 | while (rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) { |
986e4858 | 522 | ttl = min(ttl, rit->ttl); |
212f57b8 FM |
523 | rrset_records.push_back(Json::object{ |
524 | {"disabled", rit->disabled}, | |
525 | {"content", makeApiRecordContent(rit->qtype, rit->content)}}); | |
986e4858 PL |
526 | rit++; |
527 | } | |
528 | while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) { | |
212f57b8 FM |
529 | rrset_comments.push_back(Json::object{ |
530 | {"modified_at", (double)cit->modified_at}, | |
531 | {"account", cit->account}, | |
532 | {"content", cit->content}}); | |
986e4858 PL |
533 | cit++; |
534 | } | |
535 | ||
536 | rrset["name"] = current_qname.toString(); | |
d5fcd583 | 537 | rrset["type"] = current_qtype.toString(); |
986e4858 PL |
538 | rrset["records"] = rrset_records; |
539 | rrset["comments"] = rrset_comments; | |
540 | rrset["ttl"] = (double)ttl; | |
ad528718 | 541 | rrsets.emplace_back(rrset); |
986e4858 PL |
542 | rrset.clear(); |
543 | rrset_records.clear(); | |
544 | rrset_comments.clear(); | |
6754ef71 CH |
545 | } |
546 | ||
986e4858 | 547 | doc["rrsets"] = rrsets; |
6754ef71 CH |
548 | } |
549 | ||
917f686a | 550 | resp->setJsonBody(doc); |
1abb81f4 CH |
551 | } |
552 | ||
212f57b8 | 553 | void productServerStatisticsFetch(map<string, string>& out) |
6ec5e728 | 554 | { |
a45303b8 | 555 | vector<string> items = S.getEntries(); |
212f57b8 | 556 | for (const string& item : items) { |
335da0ba | 557 | out[item] = std::to_string(S.read(item)); |
a45303b8 CH |
558 | } |
559 | ||
560 | // add uptime | |
c509c9fa | 561 | out["uptime"] = std::to_string(time(nullptr) - g_starttime); |
c67bf8c5 CH |
562 | } |
563 | ||
1a09e47b | 564 | std::optional<uint64_t> productServerStatisticsFetch(const std::string& name) |
5376a5d7 RG |
565 | { |
566 | try { | |
567 | // ::read() calls ::exists() which throws a PDNSException when the key does not exist | |
568 | return S.read(name); | |
569 | } | |
1a09e47b RG |
570 | catch (...) { |
571 | return std::nullopt; | |
5376a5d7 RG |
572 | } |
573 | } | |
574 | ||
ad528718 | 575 | static void validateGatheredRRType(const DNSResourceRecord& resourceRecord) |
212f57b8 | 576 | { |
ad528718 FM |
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"); | |
24ded6cc CHB |
579 | } |
580 | } | |
581 | ||
212f57b8 FM |
582 | static void gatherRecords(const Json& container, const DNSName& qname, const QType& qtype, const uint32_t ttl, vector<DNSResourceRecord>& new_records) |
583 | { | |
ad528718 FM |
584 | DNSResourceRecord resourceRecord; |
585 | resourceRecord.qname = qname; | |
586 | resourceRecord.qtype = qtype; | |
587 | resourceRecord.auth = true; | |
588 | resourceRecord.ttl = ttl; | |
24ded6cc | 589 | |
ad528718 | 590 | validateGatheredRRType(resourceRecord); |
7f201325 | 591 | const auto& items = container["records"].array_items(); |
212f57b8 | 592 | for (const auto& record : items) { |
1f68b185 | 593 | string content = stringFromJson(record, "content"); |
36852ff8 PD |
594 | if (record.object_items().count("priority") > 0) { |
595 | throw std::runtime_error("`priority` element is not allowed in record"); | |
596 | } | |
ad528718 | 597 | resourceRecord.disabled = false; |
212f57b8 | 598 | if (!record["disabled"].is_null()) { |
ad528718 | 599 | resourceRecord.disabled = boolFromJson(record, "disabled"); |
70aad565 | 600 | } |
1f68b185 | 601 | |
1f68b185 CH |
602 | // validate that the client sent something we can actually parse, and require that data to be dotted. |
603 | try { | |
ad528718 FM |
604 | if (resourceRecord.qtype.getCode() != QType::AAAA) { |
605 | string tmp = makeApiRecordContent(resourceRecord.qtype, content); | |
1f68b185 | 606 | if (!pdns_iequals(tmp, content)) { |
212f57b8 | 607 | throw std::runtime_error("Not in expected format (parsed as '" + tmp + "')"); |
1f68b185 | 608 | } |
212f57b8 FM |
609 | } |
610 | else { | |
ad528718 FM |
611 | struct in6_addr tmpbuf |
612 | { | |
613 | }; | |
1f68b185 CH |
614 | if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) { |
615 | throw std::runtime_error("Invalid IPv6 address"); | |
1e5b9ab9 | 616 | } |
f63168e6 | 617 | } |
ad528718 | 618 | resourceRecord.content = makeBackendRecordContent(resourceRecord.qtype, content); |
1f68b185 | 619 | } |
212f57b8 | 620 | catch (std::exception& e) { |
ad528718 | 621 | throw ApiException("Record " + resourceRecord.qname.toString() + "/" + resourceRecord.qtype.toString() + " '" + content + "': " + e.what()); |
1f68b185 | 622 | } |
f63168e6 | 623 | |
ad528718 | 624 | new_records.push_back(resourceRecord); |
f63168e6 CH |
625 | } |
626 | } | |
627 | ||
212f57b8 FM |
628 | static void gatherComments(const Json& container, const DNSName& qname, const QType& qtype, vector<Comment>& new_comments) |
629 | { | |
ad528718 FM |
630 | Comment comment; |
631 | comment.qname = qname; | |
632 | comment.qtype = qtype; | |
f63168e6 | 633 | |
4646277d | 634 | time_t now = time(nullptr); |
ad528718 | 635 | for (const auto& currentComment : container["comments"].array_items()) { |
cf17e6b8 | 636 | // FIXME 2036 issue internally in uintFromJson |
ad528718 FM |
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); | |
f63168e6 CH |
641 | } |
642 | } | |
6cc98ddf | 643 | |
212f57b8 FM |
644 | static void checkDefaultDNSSECAlgos() |
645 | { | |
986e4858 PL |
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"); | |
650 | ||
651 | // Sanity check DNSSEC parameters | |
ad528718 FM |
652 | if (!::arg()["default-zsk-algorithm"].empty()) { |
653 | if (k_algo == -1) { | |
986e4858 | 654 | throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]); |
ad528718 FM |
655 | } |
656 | if (k_algo <= 10 && k_size == 0) { | |
212f57b8 | 657 | throw ApiException("default-ksk-algorithm is set to an algorithm(" + ::arg()["default-ksk-algorithm"] + ") that requires a non-zero default-ksk-size!"); |
ad528718 | 658 | } |
986e4858 PL |
659 | } |
660 | ||
ad528718 FM |
661 | if (!::arg()["default-zsk-algorithm"].empty()) { |
662 | if (z_algo == -1) { | |
986e4858 | 663 | throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]); |
ad528718 FM |
664 | } |
665 | if (z_algo <= 10 && z_size == 0) { | |
212f57b8 | 666 | throw ApiException("default-zsk-algorithm is set to an algorithm(" + ::arg()["default-zsk-algorithm"] + ") that requires a non-zero default-zsk-size!"); |
ad528718 | 667 | } |
986e4858 PL |
668 | } |
669 | } | |
670 | ||
212f57b8 FM |
671 | static void throwUnableToSecure(const DNSName& zonename) |
672 | { | |
89a7e706 | 673 | throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC" |
212f57b8 | 674 | + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration."); |
89a7e706 PL |
675 | } |
676 | ||
8ea32ea1 CH |
677 | /* |
678 | * Add KSK and ZSK to an existing zone. Algorithms and sizes will be chosen per configuration. | |
212f57b8 | 679 | */ |
ad528718 | 680 | static void addDefaultDNSSECKeys(DNSSECKeeper& dnssecKeeper, const DNSName& zonename) |
212f57b8 | 681 | { |
8ea32ea1 CH |
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"); | |
687 | ||
688 | if (k_algo != -1) { | |
ad528718 FM |
689 | int64_t keyID{-1}; |
690 | if (!dnssecKeeper.addKey(zonename, true, k_algo, keyID, k_size)) { | |
8ea32ea1 CH |
691 | throwUnableToSecure(zonename); |
692 | } | |
693 | } | |
694 | ||
695 | if (z_algo != -1) { | |
ad528718 FM |
696 | int64_t keyID{-1}; |
697 | if (!dnssecKeeper.addKey(zonename, false, z_algo, keyID, z_size)) { | |
8ea32ea1 CH |
698 | throwUnableToSecure(zonename); |
699 | } | |
700 | } | |
701 | } | |
702 | ||
ad528718 | 703 | static bool isZoneApiRectifyEnabled(const DomainInfo& domainInfo) |
212f57b8 | 704 | { |
d71d2c88 | 705 | string api_rectify; |
ad528718 | 706 | domainInfo.backend->getDomainMetadataOne(domainInfo.zone, "API-RECTIFY", api_rectify); |
d71d2c88 CH |
707 | if (api_rectify.empty() && ::arg().mustDo("default-api-rectify")) { |
708 | api_rectify = "1"; | |
709 | } | |
710 | return api_rectify == "1"; | |
711 | } | |
712 | ||
d525b58b | 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) |
a0930e45 | 714 | { |
4c70ffb3 CH |
715 | if (document["kind"].is_string()) { |
716 | kind = DomainInfo::stringToKind(stringFromJson(document, "kind")); | |
212f57b8 FM |
717 | } |
718 | else { | |
4c70ffb3 CH |
719 | kind = boost::none; |
720 | } | |
721 | ||
a190938f | 722 | if (document["masters"].is_array()) { |
d525b58b | 723 | primaries = vector<ComboAddress>(); |
212f57b8 | 724 | for (const auto& value : document["masters"].array_items()) { |
d525b58b | 725 | string primary = value.string_value(); |
ad528718 | 726 | if (primary.empty()) { |
d525b58b | 727 | throw ApiException("Primary can not be an empty string"); |
ad528718 | 728 | } |
a190938f | 729 | try { |
d525b58b | 730 | primaries->emplace_back(primary, 53); |
212f57b8 FM |
731 | } |
732 | catch (const PDNSException& e) { | |
d525b58b | 733 | throw ApiException("Primary (" + primary + ") is not an IP address: " + e.reason); |
a190938f | 734 | } |
27efa4be | 735 | } |
212f57b8 FM |
736 | } |
737 | else { | |
d525b58b | 738 | primaries = boost::none; |
986e4858 | 739 | } |
4c70ffb3 | 740 | |
a0930e45 KM |
741 | if (document["catalog"].is_string()) { |
742 | string catstring = document["catalog"].string_value(); | |
743 | catalog = (!catstring.empty() ? DNSName(catstring) : DNSName()); | |
744 | } | |
745 | else { | |
746 | catalog = boost::none; | |
747 | } | |
748 | ||
4c70ffb3 CH |
749 | if (document["account"].is_string()) { |
750 | account = document["account"].string_value(); | |
212f57b8 FM |
751 | } |
752 | else { | |
4c70ffb3 | 753 | account = boost::none; |
986e4858 | 754 | } |
4c70ffb3 CH |
755 | } |
756 | ||
c67dfcb4 CH |
757 | /* |
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 | |
212f57b8 | 761 | */ |
ad528718 | 762 | static void extractJsonTSIGKeyIds(UeberBackend& backend, const Json& jsonArray, vector<string>& metadata) |
212f57b8 FM |
763 | { |
764 | for (const auto& value : jsonArray.array_items()) { | |
c67dfcb4 CH |
765 | auto keyname(apiZoneIdToName(value.string_value())); |
766 | DNSName keyAlgo; | |
767 | string keyContent; | |
ad528718 | 768 | if (!backend.getTSIGKey(keyname, keyAlgo, keyContent)) { |
212f57b8 | 769 | throw ApiException("A TSIG key with the name '" + keyname.toLogString() + "' does not exist"); |
c67dfcb4 CH |
770 | } |
771 | metadata.push_back(keyname.toString()); | |
772 | } | |
773 | } | |
774 | ||
002de4d7 | 775 | // Must be called within backend transaction. |
ad528718 | 776 | static void updateDomainSettingsFromDocument(UeberBackend& backend, DomainInfo& domainInfo, const DNSName& zonename, const Json& document, bool zoneWasModified) |
212f57b8 | 777 | { |
4c70ffb3 | 778 | boost::optional<DomainInfo::DomainKind> kind; |
d525b58b | 779 | boost::optional<vector<ComboAddress>> primaries; |
a0930e45 | 780 | boost::optional<DNSName> catalog; |
a190938f | 781 | boost::optional<string> account; |
4c70ffb3 | 782 | |
d525b58b | 783 | extractDomainInfoFromDocument(document, kind, primaries, catalog, account); |
4c70ffb3 CH |
784 | |
785 | if (kind) { | |
ad528718 FM |
786 | domainInfo.backend->setKind(zonename, *kind); |
787 | domainInfo.kind = *kind; | |
4c70ffb3 | 788 | } |
d525b58b | 789 | if (primaries) { |
ad528718 | 790 | domainInfo.backend->setPrimaries(zonename, *primaries); |
4c70ffb3 | 791 | } |
a0930e45 | 792 | if (catalog) { |
ad528718 | 793 | domainInfo.backend->setCatalog(zonename, *catalog); |
a0930e45 | 794 | } |
4c70ffb3 | 795 | if (account) { |
ad528718 | 796 | domainInfo.backend->setAccount(zonename, *account); |
4c70ffb3 CH |
797 | } |
798 | ||
1f68b185 | 799 | if (document["soa_edit_api"].is_string()) { |
ad528718 | 800 | domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value()); |
d29d5db7 | 801 | } |
1f68b185 | 802 | if (document["soa_edit"].is_string()) { |
ad528718 | 803 | domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value()); |
6bb25159 | 804 | } |
c00908f1 PL |
805 | try { |
806 | bool api_rectify = boolFromJson(document, "api_rectify"); | |
ad528718 | 807 | domainInfo.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0"); |
986e4858 | 808 | } |
212f57b8 FM |
809 | catch (const JsonException&) { |
810 | } | |
986e4858 | 811 | |
ad528718 | 812 | DNSSECKeeper dnssecKeeper(&backend); |
168a76b3 | 813 | bool shouldRectify = zoneWasModified; |
986e4858 PL |
814 | bool dnssecInJSON = false; |
815 | bool dnssecDocVal = false; | |
df434f42 | 816 | bool nsec3paramInJSON = false; |
efc52697 | 817 | bool updateNsec3Param = false; |
df434f42 | 818 | string nsec3paramDocVal; |
986e4858 PL |
819 | |
820 | try { | |
821 | dnssecDocVal = boolFromJson(document, "dnssec"); | |
822 | dnssecInJSON = true; | |
823 | } | |
212f57b8 FM |
824 | catch (const JsonException&) { |
825 | } | |
986e4858 | 826 | |
df434f42 PL |
827 | try { |
828 | nsec3paramDocVal = stringFromJson(document, "nsec3param"); | |
829 | nsec3paramInJSON = true; | |
830 | } | |
212f57b8 FM |
831 | catch (const JsonException&) { |
832 | } | |
df434f42 | 833 | |
ad528718 FM |
834 | bool isDNSSECZone = dnssecKeeper.isSecuredZone(zonename); |
835 | bool isPresigned = dnssecKeeper.isPresigned(zonename); | |
986e4858 PL |
836 | |
837 | if (dnssecInJSON) { | |
838 | if (dnssecDocVal) { | |
839 | if (!isDNSSECZone) { | |
ad528718 | 840 | addDefaultDNSSECKeys(dnssecKeeper, zonename); |
986e4858 PL |
841 | |
842 | // Used later for NSEC3PARAM | |
ad528718 | 843 | isDNSSECZone = dnssecKeeper.isSecuredZone(zonename); |
986e4858 PL |
844 | |
845 | if (!isDNSSECZone) { | |
89a7e706 | 846 | throwUnableToSecure(zonename); |
986e4858 PL |
847 | } |
848 | shouldRectify = true; | |
efc52697 | 849 | updateNsec3Param = true; |
986e4858 | 850 | } |
212f57b8 FM |
851 | } |
852 | else { | |
986e4858 PL |
853 | // "dnssec": false in json |
854 | if (isDNSSECZone) { | |
ad528718 FM |
855 | string info; |
856 | string error; | |
857 | if (!dnssecKeeper.unSecureZone(zonename, error)) { | |
212f57b8 | 858 | throw ApiException("Error while un-securing zone '" + zonename.toString() + "': " + error); |
cbe8b186 | 859 | } |
ad528718 | 860 | isDNSSECZone = dnssecKeeper.isSecuredZone(zonename, false); |
cbe8b186 | 861 | if (isDNSSECZone) { |
212f57b8 | 862 | throw ApiException("Unable to un-secure zone '" + zonename.toString() + "'"); |
cbe8b186 PL |
863 | } |
864 | shouldRectify = true; | |
efc52697 | 865 | updateNsec3Param = true; |
986e4858 PL |
866 | } |
867 | } | |
868 | } | |
869 | ||
efc52697 | 870 | if (nsec3paramInJSON || updateNsec3Param) { |
986e4858 | 871 | shouldRectify = true; |
efc52697 KM |
872 | if (!isDNSSECZone && !nsec3paramDocVal.empty()) { |
873 | throw ApiException("NSEC3PARAM value provided for zone '" + zonename.toString() + "', but zone is not DNSSEC secured."); | |
986e4858 | 874 | } |
df434f42 | 875 | |
efc52697 | 876 | if (nsec3paramDocVal.empty()) { |
df434f42 | 877 | // Switch to NSEC |
ad528718 | 878 | if (!dnssecKeeper.unsetNSEC3PARAM(zonename)) { |
df434f42 PL |
879 | throw ApiException("Unable to remove NSEC3PARAMs from zone '" + zonename.toString()); |
880 | } | |
986e4858 | 881 | } |
efc52697 | 882 | else { |
df434f42 PL |
883 | // Set the NSEC3PARAMs |
884 | NSEC3PARAMRecordContent ns3pr(nsec3paramDocVal); | |
ad528718 FM |
885 | string error_msg; |
886 | if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) { | |
212f57b8 | 887 | throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg); |
df434f42 | 888 | } |
ad528718 | 889 | if (!dnssecKeeper.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) { |
212f57b8 | 890 | throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' passed our basic sanity checks, but cannot be used with the current backend."); |
df434f42 | 891 | } |
986e4858 PL |
892 | } |
893 | } | |
894 | ||
cf0541a3 | 895 | if (shouldRectify && !isPresigned) { |
a843c67e | 896 | // Rectify |
ad528718 | 897 | if (isZoneApiRectifyEnabled(domainInfo)) { |
a843c67e KM |
898 | string info; |
899 | string error_msg; | |
ad528718 | 900 | if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false) && !domainInfo.isSecondaryType()) { |
26636b05 | 901 | // for Secondary zones, it is possible that rectifying was not needed (example: empty zone). |
a843c67e KM |
902 | throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg); |
903 | } | |
904 | } | |
905 | ||
906 | // Increase serial | |
907 | string soa_edit_api_kind; | |
ad528718 | 908 | domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind); |
a843c67e | 909 | if (!soa_edit_api_kind.empty()) { |
ad528718 FM |
910 | SOAData soaData; |
911 | if (!backend.getSOAUncached(zonename, soaData)) { | |
a843c67e | 912 | return; |
ad528718 | 913 | } |
a843c67e KM |
914 | |
915 | string soa_edit_kind; | |
ad528718 | 916 | domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind); |
a843c67e | 917 | |
ad528718 FM |
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))) { | |
a843c67e KM |
921 | throw ApiException("Hosting backend does not support editing records."); |
922 | } | |
923 | } | |
a843c67e | 924 | } |
986e4858 | 925 | } |
1096d493 PL |
926 | |
927 | if (!document["master_tsig_key_ids"].is_null()) { | |
928 | vector<string> metadata; | |
ad528718 FM |
929 | extractJsonTSIGKeyIds(backend, document["master_tsig_key_ids"], metadata); |
930 | if (!domainInfo.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) { | |
d525b58b | 931 | throw HttpInternalServerErrorException("Unable to set new TSIG primary keys for zone '" + zonename.toLogString() + "'"); |
1096d493 PL |
932 | } |
933 | } | |
934 | if (!document["slave_tsig_key_ids"].is_null()) { | |
935 | vector<string> metadata; | |
ad528718 FM |
936 | extractJsonTSIGKeyIds(backend, document["slave_tsig_key_ids"], metadata); |
937 | if (!domainInfo.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) { | |
c02c999b | 938 | throw HttpInternalServerErrorException("Unable to set new TSIG secondary keys for zone '" + zonename.toLogString() + "'"); |
1096d493 PL |
939 | } |
940 | } | |
bb9fd223 CH |
941 | } |
942 | ||
212f57b8 FM |
943 | static bool isValidMetadataKind(const string& kind, bool readonly) |
944 | { | |
945 | static vector<string> builtinOptions{ | |
24e11043 CJ |
946 | "ALLOW-AXFR-FROM", |
947 | "AXFR-SOURCE", | |
948 | "ALLOW-DNSUPDATE-FROM", | |
949 | "TSIG-ALLOW-DNSUPDATE", | |
950 | "FORWARD-DNSUPDATE", | |
951 | "SOA-EDIT-DNSUPDATE", | |
4c5b6925 | 952 | "NOTIFY-DNSUPDATE", |
24e11043 CJ |
953 | "ALSO-NOTIFY", |
954 | "AXFR-MASTER-TSIG", | |
b08f1315 OM |
955 | "GSS-ALLOW-AXFR-PRINCIPAL", |
956 | "GSS-ACCEPTOR-PRINCIPAL", | |
24e11043 CJ |
957 | "IXFR", |
958 | "LUA-AXFR-SCRIPT", | |
959 | "NSEC3NARROW", | |
960 | "NSEC3PARAM", | |
961 | "PRESIGNED", | |
962 | "PUBLISH-CDNSKEY", | |
963 | "PUBLISH-CDS", | |
c750c26e | 964 | "SLAVE-RENOTIFY", |
24e11043 CJ |
965 | "SOA-EDIT", |
966 | "TSIG-ALLOW-AXFR", | |
1dd16193 FM |
967 | "TSIG-ALLOW-DNSUPDATE", |
968 | }; | |
24e11043 CJ |
969 | |
970 | // the following options do not allow modifications via API | |
212f57b8 | 971 | static vector<string> protectedOptions{ |
986e4858 | 972 | "API-RECTIFY", |
2fabf5ce | 973 | "AXFR-MASTER-TSIG", |
24e11043 CJ |
974 | "NSEC3NARROW", |
975 | "NSEC3PARAM", | |
976 | "PRESIGNED", | |
2fabf5ce | 977 | "LUA-AXFR-SCRIPT", |
1dd16193 FM |
978 | "TSIG-ALLOW-AXFR", |
979 | }; | |
24e11043 | 980 | |
ad528718 | 981 | if (kind.find("X-") == 0) { |
9ac4e6d5 | 982 | return true; |
ad528718 | 983 | } |
9ac4e6d5 | 984 | |
24e11043 CJ |
985 | bool found = false; |
986 | ||
ad528718 FM |
987 | for (const string& builtinOption : builtinOptions) { |
988 | if (kind == builtinOption) { | |
989 | for (const string& protectedOption : protectedOptions) { | |
990 | if (!readonly && builtinOption == protectedOption) { | |
24e11043 | 991 | return false; |
ad528718 | 992 | } |
24e11043 CJ |
993 | } |
994 | found = true; | |
995 | break; | |
996 | } | |
997 | } | |
998 | ||
999 | return found; | |
1000 | } | |
1001 | ||
917f686a KF |
1002 | /* Return OpenAPI document describing the supported API. |
1003 | */ | |
1004 | #include "apidocfiles.h" | |
1005 | ||
212f57b8 FM |
1006 | void apiDocs(HttpRequest* req, HttpResponse* resp) |
1007 | { | |
917f686a KF |
1008 | if (req->accept_yaml) { |
1009 | resp->setYamlBody(g_api_swagger_yaml); | |
212f57b8 FM |
1010 | } |
1011 | else if (req->accept_json) { | |
917f686a | 1012 | resp->setJsonBody(g_api_swagger_json); |
212f57b8 FM |
1013 | } |
1014 | else { | |
917f686a KF |
1015 | resp->setPlainBody(g_api_swagger_yaml); |
1016 | } | |
1017 | } | |
1018 | ||
7330f904 FM |
1019 | class ZoneData |
1020 | { | |
1021 | public: | |
1022 | ZoneData(HttpRequest* req) : | |
1023 | zoneName(apiZoneIdToName((req)->parameters["id"])), | |
1024 | dnssecKeeper(DNSSECKeeper{&backend}) | |
1025 | { | |
1026 | try { | |
1027 | if (!backend.getDomainInfo(zoneName, domainInfo)) { | |
1028 | throw HttpNotFoundException(); | |
1029 | } | |
1030 | } | |
1031 | catch (const PDNSException& e) { | |
1032 | throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason); | |
1033 | } | |
1034 | } | |
1035 | ||
1036 | DNSName zoneName; | |
1037 | UeberBackend backend{}; | |
1038 | DNSSECKeeper dnssecKeeper; | |
1039 | DomainInfo domainInfo{}; | |
1040 | }; | |
1041 | ||
212f57b8 FM |
1042 | static void apiZoneMetadataGET(HttpRequest* req, HttpResponse* resp) |
1043 | { | |
7330f904 | 1044 | ZoneData zoneData{req}; |
24e11043 | 1045 | |
212f57b8 | 1046 | map<string, vector<string>> metas; |
1929764e | 1047 | Json::array document; |
24e11043 | 1048 | |
7330f904 | 1049 | if (!zoneData.backend.getAllDomainMetadata(zoneData.zoneName, metas)) { |
1929764e AT |
1050 | throw HttpNotFoundException(); |
1051 | } | |
24e11043 | 1052 | |
1929764e AT |
1053 | for (const auto& meta : metas) { |
1054 | Json::array entries; | |
1055 | for (const string& value : meta.second) { | |
ad528718 | 1056 | entries.emplace_back(value); |
1929764e | 1057 | } |
24e11043 | 1058 | |
212f57b8 FM |
1059 | Json::object key{ |
1060 | {"type", "Metadata"}, | |
1061 | {"kind", meta.first}, | |
1062 | {"metadata", entries}}; | |
ad528718 | 1063 | document.emplace_back(key); |
1929764e AT |
1064 | } |
1065 | resp->setJsonBody(document); | |
1066 | } | |
24e11043 | 1067 | |
212f57b8 FM |
1068 | static void apiZoneMetadataPOST(HttpRequest* req, HttpResponse* resp) |
1069 | { | |
7330f904 | 1070 | ZoneData zoneData{req}; |
24e11043 | 1071 | |
1929764e AT |
1072 | const auto& document = req->json(); |
1073 | string kind; | |
1074 | vector<string> entries; | |
24e11043 | 1075 | |
1929764e AT |
1076 | try { |
1077 | kind = stringFromJson(document, "kind"); | |
212f57b8 FM |
1078 | } |
1079 | catch (const JsonException&) { | |
1080 | throw ApiException("kind is not specified or not a string"); | |
1929764e | 1081 | } |
24e11043 | 1082 | |
1929764e AT |
1083 | if (!isValidMetadataKind(kind, false)) { |
1084 | throw ApiException("Unsupported metadata kind '" + kind + "'"); | |
1085 | } | |
24e11043 | 1086 | |
1929764e | 1087 | vector<string> vecMetadata; |
c6720e79 | 1088 | |
7330f904 FM |
1089 | if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, vecMetadata)) { |
1090 | throw ApiException("Could not retrieve metadata entries for domain '" + zoneData.zoneName.toString() + "'"); | |
1929764e | 1091 | } |
c6720e79 | 1092 | |
1929764e AT |
1093 | const auto& metadata = document["metadata"]; |
1094 | if (!metadata.is_array()) { | |
1095 | throw ApiException("metadata is not specified or not an array"); | |
1096 | } | |
24e11043 | 1097 | |
1929764e AT |
1098 | for (const auto& value : metadata.array_items()) { |
1099 | if (!value.is_string()) { | |
1100 | throw ApiException("metadata must be strings"); | |
1101 | } | |
1102 | if (std::find(vecMetadata.cbegin(), | |
212f57b8 FM |
1103 | vecMetadata.cend(), |
1104 | value.string_value()) | |
1105 | == vecMetadata.cend()) { | |
1929764e | 1106 | vecMetadata.push_back(value.string_value()); |
24e11043 | 1107 | } |
1929764e AT |
1108 | } |
1109 | ||
7330f904 FM |
1110 | if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) { |
1111 | throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'"); | |
1929764e | 1112 | } |
24e11043 | 1113 | |
7330f904 | 1114 | DNSSECKeeper::clearMetaCache(zoneData.zoneName); |
c6720e79 | 1115 | |
1929764e AT |
1116 | Json::array respMetadata; |
1117 | for (const string& value : vecMetadata) { | |
ad528718 | 1118 | respMetadata.emplace_back(value); |
1929764e | 1119 | } |
d30fff94 | 1120 | |
212f57b8 FM |
1121 | Json::object key{ |
1122 | {"type", "Metadata"}, | |
1123 | {"kind", document["kind"]}, | |
1124 | {"metadata", respMetadata}}; | |
c6720e79 | 1125 | |
1929764e AT |
1126 | resp->status = 201; |
1127 | resp->setJsonBody(key); | |
1128 | } | |
24e11043 | 1129 | |
212f57b8 FM |
1130 | static void apiZoneMetadataKindGET(HttpRequest* req, HttpResponse* resp) |
1131 | { | |
7330f904 | 1132 | ZoneData zoneData{req}; |
d38e81e6 PL |
1133 | |
1134 | string kind = req->parameters["kind"]; | |
24e11043 | 1135 | |
a406b334 AT |
1136 | vector<string> metadata; |
1137 | Json::object document; | |
1138 | Json::array entries; | |
24e11043 | 1139 | |
7330f904 | 1140 | if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, metadata)) { |
a406b334 AT |
1141 | throw HttpNotFoundException(); |
1142 | } | |
1143 | if (!isValidMetadataKind(kind, true)) { | |
1144 | throw ApiException("Unsupported metadata kind '" + kind + "'"); | |
1145 | } | |
24e11043 | 1146 | |
a406b334 AT |
1147 | document["type"] = "Metadata"; |
1148 | document["kind"] = kind; | |
24e11043 | 1149 | |
a406b334 | 1150 | for (const string& value : metadata) { |
ad528718 | 1151 | entries.emplace_back(value); |
a406b334 AT |
1152 | } |
1153 | ||
1154 | document["metadata"] = entries; | |
1155 | resp->setJsonBody(document); | |
1156 | } | |
1157 | ||
212f57b8 FM |
1158 | static void apiZoneMetadataKindPUT(HttpRequest* req, HttpResponse* resp) |
1159 | { | |
7330f904 | 1160 | ZoneData zoneData{req}; |
24e11043 | 1161 | |
a406b334 | 1162 | string kind = req->parameters["kind"]; |
24e11043 | 1163 | |
a406b334 | 1164 | const auto& document = req->json(); |
24e11043 | 1165 | |
a406b334 AT |
1166 | if (!isValidMetadataKind(kind, false)) { |
1167 | throw ApiException("Unsupported metadata kind '" + kind + "'"); | |
1168 | } | |
24e11043 | 1169 | |
a406b334 AT |
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"); | |
1174 | } | |
1175 | for (const auto& value : metadata.array_items()) { | |
1176 | if (!value.is_string()) { | |
1177 | throw ApiException("metadata must be strings"); | |
24e11043 | 1178 | } |
a406b334 AT |
1179 | vecMetadata.push_back(value.string_value()); |
1180 | } | |
24e11043 | 1181 | |
7330f904 FM |
1182 | if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) { |
1183 | throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'"); | |
a406b334 | 1184 | } |
24e11043 | 1185 | |
7330f904 | 1186 | DNSSECKeeper::clearMetaCache(zoneData.zoneName); |
d30fff94 | 1187 | |
212f57b8 FM |
1188 | Json::object key{ |
1189 | {"type", "Metadata"}, | |
1190 | {"kind", kind}, | |
1191 | {"metadata", metadata}}; | |
24e11043 | 1192 | |
a406b334 AT |
1193 | resp->setJsonBody(key); |
1194 | } | |
24e11043 | 1195 | |
212f57b8 FM |
1196 | static void apiZoneMetadataKindDELETE(HttpRequest* req, HttpResponse* resp) |
1197 | { | |
7330f904 | 1198 | ZoneData zoneData{req}; |
d30fff94 | 1199 | |
a406b334 AT |
1200 | const string& kind = req->parameters["kind"]; |
1201 | if (!isValidMetadataKind(kind, false)) { | |
1202 | throw ApiException("Unsupported metadata kind '" + kind + "'"); | |
1203 | } | |
1204 | ||
212f57b8 | 1205 | vector<string> metadata; // an empty vector will do it |
7330f904 FM |
1206 | if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, metadata)) { |
1207 | throw ApiException("Could not delete metadata for domain '" + zoneData.zoneName.toString() + "' (" + kind + ")"); | |
a406b334 AT |
1208 | } |
1209 | ||
7330f904 | 1210 | DNSSECKeeper::clearMetaCache(zoneData.zoneName); |
a406b334 AT |
1211 | resp->status = 204; |
1212 | } | |
1213 | ||
71de13d7 | 1214 | // Throws 404 if the key with inquireKeyId does not exist |
ad528718 | 1215 | static void apiZoneCryptoKeysCheckKeyExists(const DNSName& zonename, int inquireKeyId, DNSSECKeeper* dnssecKeeper) |
212f57b8 | 1216 | { |
ad528718 | 1217 | DNSSECKeeper::keyset_t keyset = dnssecKeeper->getKeys(zonename, false); |
71de13d7 | 1218 | bool found = false; |
212f57b8 FM |
1219 | for (const auto& value : keyset) { |
1220 | if (value.second.id == (unsigned)inquireKeyId) { | |
71de13d7 PL |
1221 | found = true; |
1222 | break; | |
1223 | } | |
1224 | } | |
1225 | if (!found) { | |
1226 | throw HttpNotFoundException(); | |
1227 | } | |
1228 | } | |
1229 | ||
212f57b8 FM |
1230 | static inline int getInquireKeyId(HttpRequest* req, const DNSName& zonename, DNSSECKeeper* dnsseckeeper) |
1231 | { | |
20f8dbd7 AT |
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); | |
1236 | } | |
1237 | return inquireKeyId; | |
1238 | } | |
1239 | ||
212f57b8 FM |
1240 | static void apiZoneCryptokeysExport(const DNSName& zonename, int64_t inquireKeyId, HttpResponse* resp, DNSSECKeeper* dnssec_dk) |
1241 | { | |
1242 | DNSSECKeeper::keyset_t keyset = dnssec_dk->getKeys(zonename, false); | |
4b7f120a | 1243 | |
997cab68 BZ |
1244 | bool inquireSingleKey = inquireKeyId >= 0; |
1245 | ||
24afabad | 1246 | Json::array doc; |
212f57b8 | 1247 | for (const auto& value : keyset) { |
997cab68 | 1248 | if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) { |
29704f66 | 1249 | continue; |
38809e97 | 1250 | } |
24afabad | 1251 | |
b6bd795c | 1252 | string keyType; |
60b0a236 | 1253 | switch (value.second.keyType) { |
212f57b8 FM |
1254 | case DNSSECKeeper::KSK: |
1255 | keyType = "ksk"; | |
1256 | break; | |
1257 | case DNSSECKeeper::ZSK: | |
1258 | keyType = "zsk"; | |
1259 | break; | |
1260 | case DNSSECKeeper::CSK: | |
1261 | keyType = "csk"; | |
1262 | break; | |
1263 | } | |
1264 | ||
1265 | Json::object key{ | |
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()}}; | |
24afabad | 1275 | |
2bb1f06c | 1276 | string publishCDS; |
20f8dbd7 | 1277 | dnssec_dk->getPublishCDS(zonename, publishCDS); |
2bb1f06c PD |
1278 | |
1279 | vector<string> digestAlgos; | |
1280 | stringtok(digestAlgos, publishCDS, ", "); | |
1281 | ||
1282 | std::set<unsigned int> CDSalgos; | |
212f57b8 | 1283 | for (auto const& digestAlgo : digestAlgos) { |
a0383aad | 1284 | CDSalgos.insert(pdns::checked_stoi<unsigned int>(digestAlgo)); |
2bb1f06c PD |
1285 | } |
1286 | ||
b6bd795c | 1287 | if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) { |
2bb1f06c | 1288 | Json::array cdses; |
24afabad | 1289 | Json::array dses; |
ad528718 | 1290 | for (const uint8_t keyid : {DNSSECKeeper::DIGEST_SHA1, DNSSECKeeper::DIGEST_SHA256, DNSSECKeeper::DIGEST_GOST, DNSSECKeeper::DIGEST_SHA384}) { |
997cab68 | 1291 | try { |
ad528718 | 1292 | string dsRecordContent = makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation(); |
2bb1f06c | 1293 | |
ad528718 | 1294 | dses.emplace_back(dsRecordContent); |
2bb1f06c | 1295 | |
ad528718 FM |
1296 | if (CDSalgos.count(keyid) != 0) { |
1297 | cdses.emplace_back(dsRecordContent); | |
212f57b8 FM |
1298 | } |
1299 | } | |
1300 | catch (...) { | |
1301 | } | |
ad528718 | 1302 | } |
2bb1f06c | 1303 | |
24afabad | 1304 | key["ds"] = dses; |
2bb1f06c | 1305 | |
ad528718 | 1306 | if (!cdses.empty()) { |
2bb1f06c PD |
1307 | key["cds"] = cdses; |
1308 | } | |
4b7f120a | 1309 | } |
29704f66 CH |
1310 | |
1311 | if (inquireSingleKey) { | |
1312 | key["privatekey"] = value.first.getKey()->convertToISC(); | |
917f686a | 1313 | resp->setJsonBody(key); |
29704f66 CH |
1314 | return; |
1315 | } | |
ad528718 | 1316 | doc.emplace_back(key); |
4b7f120a MS |
1317 | } |
1318 | ||
29704f66 CH |
1319 | if (inquireSingleKey) { |
1320 | // we came here because we couldn't find the requested key. | |
1321 | throw HttpNotFoundException(); | |
1322 | } | |
917f686a | 1323 | resp->setJsonBody(doc); |
20f8dbd7 | 1324 | } |
997cab68 | 1325 | |
212f57b8 FM |
1326 | static void apiZoneCryptokeysGET(HttpRequest* req, HttpResponse* resp) |
1327 | { | |
7330f904 FM |
1328 | ZoneData zoneData{req}; |
1329 | const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper); | |
20f8dbd7 | 1330 | |
7330f904 | 1331 | apiZoneCryptokeysExport(zoneData.zoneName, inquireKeyId, resp, &zoneData.dnssecKeeper); |
997cab68 BZ |
1332 | } |
1333 | ||
1334 | /* | |
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. | |
1337 | * Server Answers: | |
60b0a236 | 1338 | * Case 1: the backend returns true on removal. This means the key is gone. |
75191dc4 | 1339 | * The server returns 204 No Content, no body. |
955cbfd0 | 1340 | * Case 2: the backend returns false on removal. An error occurred. |
75191dc4 PL |
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 | |
997cab68 | 1344 | * */ |
212f57b8 FM |
1345 | static void apiZoneCryptokeysDELETE(HttpRequest* req, HttpResponse* resp) |
1346 | { | |
7330f904 FM |
1347 | ZoneData zoneData{req}; |
1348 | const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper); | |
20f8dbd7 AT |
1349 | |
1350 | if (inquireKeyId == -1) { | |
212f57b8 | 1351 | throw HttpBadRequestException(); |
20f8dbd7 AT |
1352 | } |
1353 | ||
7330f904 | 1354 | if (zoneData.dnssecKeeper.removeKey(zoneData.zoneName, inquireKeyId)) { |
60b0a236 | 1355 | resp->body = ""; |
75191dc4 | 1356 | resp->status = 204; |
212f57b8 FM |
1357 | } |
1358 | else { | |
997cab68 BZ |
1359 | resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422); |
1360 | } | |
1361 | } | |
1362 | ||
1363 | /* | |
1364 | * This method adds a key to a zone by generate it or content parameter. | |
1365 | * Parameter: | |
1366 | * { | |
5d9c6182 | 1367 | * "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string> |
997cab68 BZ |
1368 | * "keytype" : "ksk|zsk" <string> |
1369 | * "active" : "true|false" <value> | |
5d9c6182 | 1370 | * "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms |
997cab68 BZ |
1371 | * "bits" : number of bits <int> |
1372 | * } | |
1373 | * | |
1374 | * Response: | |
1375 | * Case 1: keytype isn't ksk|zsk | |
1376 | * The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"} | |
60b0a236 BZ |
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."} | |
5d9c6182 | 1379 | * Case 3: The "algorithm" isn't supported |
997cab68 | 1380 | * The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"} |
60b0a236 | 1381 | * Case 4: Algorithm <= 10 and no bits were passed |
997cab68 | 1382 | * The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"} |
60b0a236 BZ |
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 | |
997cab68 | 1388 | * The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"} |
60b0a236 BZ |
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 | |
997cab68 BZ |
1397 | */ |
1398 | ||
212f57b8 FM |
1399 | static void apiZoneCryptokeysPOST(HttpRequest* req, HttpResponse* resp) |
1400 | { | |
7330f904 | 1401 | ZoneData zoneData{req}; |
20f8dbd7 AT |
1402 | |
1403 | const auto& document = req->json(); | |
5d9c6182 PL |
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"; | |
1410 | } | |
997cab68 | 1411 | bool active = boolFromJson(document, "active", false); |
33918299 | 1412 | bool published = boolFromJson(document, "published", true); |
ad528718 | 1413 | bool keyOrZone = false; |
60b0a236 | 1414 | |
eefd15b3 | 1415 | if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") { |
997cab68 | 1416 | keyOrZone = true; |
212f57b8 FM |
1417 | } |
1418 | else if (stringFromJson(document, "keytype") == "zsk") { | |
997cab68 | 1419 | keyOrZone = false; |
212f57b8 FM |
1420 | } |
1421 | else { | |
997cab68 BZ |
1422 | throw ApiException("Invalid keytype " + stringFromJson(document, "keytype")); |
1423 | } | |
1424 | ||
b727e19b | 1425 | int64_t insertedId = -1; |
997cab68 | 1426 | |
5d9c6182 | 1427 | if (privatekey.is_null()) { |
43215ca6 | 1428 | int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size"); |
60b0a236 BZ |
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"); | |
212f57b8 | 1433 | } |
ad528718 FM |
1434 | |
1435 | bits = docbits.int_value(); | |
60b0a236 | 1436 | } |
43215ca6 | 1437 | int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]); |
20f8dbd7 | 1438 | const auto& providedAlgo = document["algorithm"]; |
997cab68 | 1439 | if (providedAlgo.is_string()) { |
60b0a236 | 1440 | algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value()); |
20f8dbd7 | 1441 | if (algorithm == -1) { |
997cab68 | 1442 | throw ApiException("Unknown algorithm: " + providedAlgo.string_value()); |
20f8dbd7 | 1443 | } |
212f57b8 FM |
1444 | } |
1445 | else if (providedAlgo.is_number()) { | |
997cab68 | 1446 | algorithm = providedAlgo.int_value(); |
212f57b8 FM |
1447 | } |
1448 | else if (!providedAlgo.is_null()) { | |
60b0a236 | 1449 | throw ApiException("Unknown algorithm: " + providedAlgo.string_value()); |
997cab68 BZ |
1450 | } |
1451 | ||
60b0a236 | 1452 | try { |
7330f904 | 1453 | if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, keyOrZone, algorithm, insertedId, bits, active, published)) { |
b727e19b RG |
1454 | throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?"); |
1455 | } | |
212f57b8 FM |
1456 | } |
1457 | catch (std::runtime_error& error) { | |
997cab68 BZ |
1458 | throw ApiException(error.what()); |
1459 | } | |
ad528718 | 1460 | if (insertedId < 0) { |
997cab68 | 1461 | throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?"); |
ad528718 | 1462 | } |
212f57b8 FM |
1463 | } |
1464 | else if (document["bits"].is_null() && document["algorithm"].is_null()) { | |
20f8dbd7 | 1465 | const auto& keyData = stringFromJson(document, privatekey_fieldname); |
997cab68 BZ |
1466 | DNSKEYRecordContent dkrc; |
1467 | DNSSECPrivateKey dpk; | |
60b0a236 | 1468 | try { |
997cab68 | 1469 | shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData)); |
a456662f RG |
1470 | uint16_t flags = 0; |
1471 | if (keyOrZone) { | |
1472 | flags = 257; | |
1473 | } | |
1474 | else { | |
1475 | flags = 256; | |
1476 | } | |
997cab68 | 1477 | |
a456662f | 1478 | uint8_t algorithm = dkrc.d_algorithm; |
a456662f RG |
1479 | // TODO remove in 4.2.0 |
1480 | if (algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1) { | |
c5153ca0 | 1481 | algorithm = DNSSECKeeper::RSASHA1; |
a456662f | 1482 | } |
c5153ca0 | 1483 | dpk.setKey(dke, flags, algorithm); |
997cab68 | 1484 | } |
60b0a236 BZ |
1485 | catch (std::runtime_error& error) { |
1486 | throw ApiException("Key could not be parsed. Make sure your key format is correct."); | |
212f57b8 FM |
1487 | } |
1488 | try { | |
7330f904 | 1489 | if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, dpk, insertedId, active, published)) { |
b727e19b RG |
1490 | throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?"); |
1491 | } | |
212f57b8 FM |
1492 | } |
1493 | catch (std::runtime_error& error) { | |
997cab68 BZ |
1494 | throw ApiException(error.what()); |
1495 | } | |
20f8dbd7 | 1496 | if (insertedId < 0) { |
997cab68 | 1497 | throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?"); |
20f8dbd7 | 1498 | } |
212f57b8 FM |
1499 | } |
1500 | else { | |
5d9c6182 | 1501 | throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields."); |
997cab68 | 1502 | } |
7330f904 | 1503 | apiZoneCryptokeysExport(zoneData.zoneName, insertedId, resp, &zoneData.dnssecKeeper); |
997cab68 | 1504 | resp->status = 201; |
60b0a236 | 1505 | } |
997cab68 BZ |
1506 | |
1507 | /* | |
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. | |
1510 | * Server Answers: | |
60b0a236 | 1511 | * Case 1: invalid JSON data |
997cab68 | 1512 | * The server returns 400 Bad Request |
60b0a236 BZ |
1513 | * Case 2: the backend returns true on de/activation. This means the key is de/active. |
1514 | * The server returns 204 No Content | |
955cbfd0 | 1515 | * Case 3: the backend returns false on de/activation. An error occurred. |
997cab68 BZ |
1516 | * The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name" |
1517 | * */ | |
212f57b8 FM |
1518 | static void apiZoneCryptokeysPUT(HttpRequest* req, HttpResponse* resp) |
1519 | { | |
7330f904 FM |
1520 | ZoneData zoneData{req}; |
1521 | const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper); | |
20f8dbd7 AT |
1522 | |
1523 | if (inquireKeyId == -1) { | |
1524 | throw HttpBadRequestException(); | |
1525 | } | |
212f57b8 | 1526 | // throws an exception if the Body is empty |
20f8dbd7 | 1527 | const auto& document = req->json(); |
212f57b8 | 1528 | // throws an exception if the key does not exist or is not a bool |
997cab68 | 1529 | bool active = boolFromJson(document, "active"); |
33918299 | 1530 | bool published = boolFromJson(document, "published", true); |
60b0a236 | 1531 | if (active) { |
7330f904 FM |
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); | |
997cab68 BZ |
1534 | return; |
1535 | } | |
212f57b8 FM |
1536 | } |
1537 | else { | |
7330f904 FM |
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); | |
997cab68 BZ |
1540 | return; |
1541 | } | |
1542 | } | |
33918299 RG |
1543 | |
1544 | if (published) { | |
7330f904 FM |
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); | |
33918299 RG |
1547 | return; |
1548 | } | |
212f57b8 FM |
1549 | } |
1550 | else { | |
7330f904 FM |
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); | |
33918299 RG |
1553 | return; |
1554 | } | |
1555 | } | |
1556 | ||
60b0a236 BZ |
1557 | resp->body = ""; |
1558 | resp->status = 204; | |
997cab68 BZ |
1559 | } |
1560 | ||
212f57b8 FM |
1561 | static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, const DNSName& zonename) |
1562 | { | |
ad528718 | 1563 | DNSResourceRecord resourceRecord; |
0f0e73fe | 1564 | vector<string> zonedata; |
1f68b185 | 1565 | stringtok(zonedata, zonestring, "\r\n"); |
0f0e73fe MS |
1566 | |
1567 | ZoneParserTNG zpt(zonedata, zonename); | |
ba3d53d1 | 1568 | zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps")); |
e3fc3ebd | 1569 | zpt.setMaxIncludes(::arg().asNum("max-include-depth")); |
0f0e73fe | 1570 | |
212f57b8 | 1571 | bool seenSOA = false; |
0f0e73fe MS |
1572 | |
1573 | string comment = "Imported via the API"; | |
1574 | ||
1575 | try { | |
ad528718 FM |
1576 | while (zpt.get(resourceRecord, &comment)) { |
1577 | if (seenSOA && resourceRecord.qtype.getCode() == QType::SOA) { | |
0f0e73fe | 1578 | continue; |
ad528718 FM |
1579 | } |
1580 | if (resourceRecord.qtype.getCode() == QType::SOA) { | |
212f57b8 | 1581 | seenSOA = true; |
ad528718 FM |
1582 | } |
1583 | validateGatheredRRType(resourceRecord); | |
0f0e73fe | 1584 | |
ad528718 | 1585 | new_records.push_back(resourceRecord); |
0f0e73fe MS |
1586 | } |
1587 | } | |
212f57b8 FM |
1588 | catch (std::exception& ae) { |
1589 | throw ApiException("An error occurred while parsing the zonedata: " + string(ae.what())); | |
0f0e73fe MS |
1590 | } |
1591 | } | |
1592 | ||
ef2ea4bf | 1593 | /** Throws ApiException if records which violate RRset constraints are present. |
e3675a8a | 1594 | * NOTE: sorts records in-place. |
646bcd7d CH |
1595 | * |
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 | |
e3675a8a | 1600 | */ |
2eb206ec KM |
1601 | static void checkNewRecords(vector<DNSResourceRecord>& records, const DNSName& zone) |
1602 | { | |
e3675a8a | 1603 | sort(records.begin(), records.end(), |
212f57b8 FM |
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); | |
1607 | }); | |
646bcd7d | 1608 | |
e3675a8a | 1609 | DNSResourceRecord previous; |
212f57b8 | 1610 | for (const auto& rec : records) { |
646bcd7d CH |
1611 | if (previous.qname == rec.qname) { |
1612 | if (previous.qtype == rec.qtype) { | |
1613 | if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) { | |
212f57b8 | 1614 | throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " has more than one record"); |
646bcd7d CH |
1615 | } |
1616 | if (previous.content == rec.content) { | |
d5fcd583 | 1617 | throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " with content \"" + rec.content + "\""); |
646bcd7d | 1618 | } |
212f57b8 FM |
1619 | } |
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"); | |
646bcd7d CH |
1622 | } |
1623 | } | |
1624 | ||
2eb206ec KM |
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"); | |
1628 | } | |
1629 | } | |
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"); | |
1632 | } | |
1633 | ||
646bcd7d CH |
1634 | // Check if the DNSNames that should be hostnames, are hostnames |
1635 | try { | |
1636 | checkHostnameCorrectness(rec); | |
212f57b8 FM |
1637 | } |
1638 | catch (const std::exception& e) { | |
1639 | throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + ": " + e.what()); | |
e3675a8a | 1640 | } |
646bcd7d | 1641 | |
e3675a8a CH |
1642 | previous = rec; |
1643 | } | |
1644 | } | |
1645 | ||
ad528718 | 1646 | static void checkTSIGKey(UeberBackend& backend, const DNSName& keyname, const DNSName& algo, const string& content) |
212f57b8 | 1647 | { |
6f16f97c PL |
1648 | DNSName algoFromDB; |
1649 | string contentFromDB; | |
ad528718 | 1650 | if (backend.getTSIGKey(keyname, algoFromDB, contentFromDB)) { |
212f57b8 | 1651 | throw HttpConflictException("A TSIG key with the name '" + keyname.toLogString() + "' already exists"); |
fa07a3cf PL |
1652 | } |
1653 | ||
ad528718 | 1654 | TSIGHashEnum the{}; |
6f16f97c PL |
1655 | if (!getTSIGHashEnum(algo, the)) { |
1656 | throw ApiException("Unknown TSIG algorithm: " + algo.toLogString()); | |
1657 | } | |
fa07a3cf | 1658 | |
6f16f97c PL |
1659 | string b64out; |
1660 | if (B64Decode(content, b64out) == -1) { | |
1661 | throw ApiException("TSIG content '" + content + "' cannot be base64-decoded"); | |
fa07a3cf | 1662 | } |
6f16f97c | 1663 | } |
fa07a3cf | 1664 | |
212f57b8 FM |
1665 | static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content) |
1666 | { | |
6f16f97c | 1667 | Json::object tsigkey = { |
212f57b8 FM |
1668 | {"name", keyname.toStringNoDot()}, |
1669 | {"id", apiZoneNameToId(keyname)}, | |
1670 | {"algorithm", algo.toStringNoDot()}, | |
1671 | {"key", content}, | |
1672 | {"type", "TSIGKey"}}; | |
6f16f97c PL |
1673 | return tsigkey; |
1674 | } | |
fa07a3cf | 1675 | |
212f57b8 FM |
1676 | static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent = true) |
1677 | { | |
519a6288 | 1678 | return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : ""); |
6f16f97c PL |
1679 | } |
1680 | ||
212f57b8 FM |
1681 | static void apiServerTSIGKeysGET(HttpRequest* /* req */, HttpResponse* resp) |
1682 | { | |
efc47b04 | 1683 | UeberBackend backend; |
2775cadd | 1684 | vector<struct TSIGKey> keys; |
6f16f97c | 1685 | |
efc47b04 | 1686 | if (!backend.getTSIGKeys(keys)) { |
2775cadd AT |
1687 | throw HttpInternalServerErrorException("Unable to retrieve TSIG keys"); |
1688 | } | |
6f16f97c | 1689 | |
2775cadd | 1690 | Json::array doc; |
6f16f97c | 1691 | |
212f57b8 | 1692 | for (const auto& key : keys) { |
ad528718 | 1693 | doc.emplace_back(makeJSONTSIGKey(key, false)); |
2775cadd AT |
1694 | } |
1695 | resp->setJsonBody(doc); | |
1696 | } | |
6f16f97c | 1697 | |
212f57b8 FM |
1698 | static void apiServerTSIGKeysPOST(HttpRequest* req, HttpResponse* resp) |
1699 | { | |
efc47b04 | 1700 | UeberBackend backend; |
2775cadd AT |
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(); | |
6f16f97c | 1705 | |
2775cadd AT |
1706 | if (content.empty()) { |
1707 | try { | |
1708 | content = makeTSIGKey(algo); | |
212f57b8 FM |
1709 | } |
1710 | catch (const PDNSException& exc) { | |
2775cadd | 1711 | throw HttpBadRequestException(exc.reason); |
6f16f97c | 1712 | } |
2775cadd | 1713 | } |
6f16f97c | 1714 | |
2775cadd | 1715 | // Will throw an ApiException or HttpConflictException on error |
efc47b04 | 1716 | checkTSIGKey(backend, keyname, algo, content); |
2775cadd | 1717 | |
efc47b04 | 1718 | if (!backend.setTSIGKey(keyname, algo, content)) { |
2775cadd | 1719 | throw HttpInternalServerErrorException("Unable to add TSIG key"); |
6f16f97c | 1720 | } |
2775cadd AT |
1721 | |
1722 | resp->status = 201; | |
1723 | resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content)); | |
1724 | } | |
1725 | ||
6ab2f63a FM |
1726 | class TSIGKeyData |
1727 | { | |
1728 | public: | |
1729 | TSIGKeyData(HttpRequest* req) : | |
1730 | keyName(apiZoneIdToName(req->parameters["id"])) | |
1731 | { | |
1732 | try { | |
1733 | if (!backend.getTSIGKey(keyName, algo, content)) { | |
1734 | throw HttpNotFoundException("TSIG key with name '" + keyName.toLogString() + "' not found"); | |
1735 | } | |
1736 | } | |
1737 | catch (const PDNSException& e) { | |
1738 | throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason); | |
1739 | } | |
1740 | ||
1741 | tsigKey.name = keyName; | |
1742 | tsigKey.algorithm = algo; | |
1743 | tsigKey.key = std::move(content); | |
1744 | } | |
1745 | ||
1746 | UeberBackend backend; | |
1747 | DNSName keyName; | |
1748 | DNSName algo; | |
1749 | string content; | |
1750 | struct TSIGKey tsigKey; | |
1751 | }; | |
fa07a3cf | 1752 | |
212f57b8 FM |
1753 | static void apiServerTSIGKeyDetailGET(HttpRequest* req, HttpResponse* resp) |
1754 | { | |
6ab2f63a | 1755 | TSIGKeyData tsigKeyData{req}; |
fa07a3cf | 1756 | |
6ab2f63a | 1757 | resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey)); |
ca740e49 | 1758 | } |
81c39bc6 | 1759 | |
212f57b8 FM |
1760 | static void apiServerTSIGKeyDetailPUT(HttpRequest* req, HttpResponse* resp) |
1761 | { | |
6ab2f63a | 1762 | TSIGKeyData tsigKeyData{req}; |
81c39bc6 | 1763 | |
ca740e49 AT |
1764 | const auto& document = req->json(); |
1765 | ||
1766 | if (document["name"].is_string()) { | |
6ab2f63a | 1767 | tsigKeyData.tsigKey.name = DNSName(document["name"].string_value()); |
ca740e49 AT |
1768 | } |
1769 | if (document["algorithm"].is_string()) { | |
6ab2f63a | 1770 | tsigKeyData.tsigKey.algorithm = DNSName(document["algorithm"].string_value()); |
ca740e49 | 1771 | |
ad528718 | 1772 | TSIGHashEnum the{}; |
6ab2f63a FM |
1773 | if (!getTSIGHashEnum(tsigKeyData.tsigKey.algorithm, the)) { |
1774 | throw ApiException("Unknown TSIG algorithm: " + tsigKeyData.tsigKey.algorithm.toLogString()); | |
81c39bc6 | 1775 | } |
ca740e49 AT |
1776 | } |
1777 | if (document["key"].is_string()) { | |
1778 | string new_content = document["key"].string_value(); | |
1779 | string decoded; | |
1780 | if (B64Decode(new_content, decoded) == -1) { | |
1781 | throw ApiException("Can not base64 decode key content '" + new_content + "'"); | |
81c39bc6 | 1782 | } |
6ab2f63a | 1783 | tsigKeyData.tsigKey.key = std::move(new_content); |
ca740e49 | 1784 | } |
6ab2f63a | 1785 | if (!tsigKeyData.backend.setTSIGKey(tsigKeyData.tsigKey.name, tsigKeyData.tsigKey.algorithm, tsigKeyData.tsigKey.key)) { |
ca740e49 AT |
1786 | throw HttpInternalServerErrorException("Unable to save TSIG Key"); |
1787 | } | |
6ab2f63a | 1788 | if (tsigKeyData.tsigKey.name != tsigKeyData.keyName) { |
ca740e49 | 1789 | // Remove the old key |
6ab2f63a FM |
1790 | if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) { |
1791 | throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'"); | |
81c39bc6 | 1792 | } |
81c39bc6 | 1793 | } |
6ab2f63a | 1794 | resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey)); |
ca740e49 AT |
1795 | } |
1796 | ||
212f57b8 FM |
1797 | static void apiServerTSIGKeyDetailDELETE(HttpRequest* req, HttpResponse* resp) |
1798 | { | |
6ab2f63a FM |
1799 | TSIGKeyData tsigKeyData{req}; |
1800 | if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) { | |
1801 | throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'"); | |
ca740e49 AT |
1802 | } |
1803 | resp->body = ""; | |
1804 | resp->status = 204; | |
1805 | } | |
1806 | ||
212f57b8 FM |
1807 | static void apiServerAutoprimaryDetailDELETE(HttpRequest* req, HttpResponse* resp) |
1808 | { | |
efc47b04 | 1809 | UeberBackend backend; |
61fb497a | 1810 | const AutoPrimary& primary{req->parameters["ip"], req->parameters["nameserver"], ""}; |
efc47b04 | 1811 | if (!backend.autoPrimaryRemove(primary)) { |
212f57b8 | 1812 | throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature"); |
61fb497a AT |
1813 | } |
1814 | resp->body = ""; | |
1815 | resp->status = 204; | |
1816 | } | |
1817 | ||
212f57b8 FM |
1818 | static void apiServerAutoprimariesGET(HttpRequest* /* req */, HttpResponse* resp) |
1819 | { | |
efc47b04 | 1820 | UeberBackend backend; |
782e9b24 | 1821 | |
241dc035 | 1822 | std::vector<AutoPrimary> primaries; |
efc47b04 | 1823 | if (!backend.autoPrimariesList(primaries)) { |
241dc035 AT |
1824 | throw HttpInternalServerErrorException("Unable to retrieve autoprimaries"); |
1825 | } | |
1826 | Json::array doc; | |
212f57b8 | 1827 | for (const auto& primary : primaries) { |
241dc035 | 1828 | const Json::object obj = { |
212f57b8 FM |
1829 | {"ip", primary.ip}, |
1830 | {"nameserver", primary.nameserver}, | |
1831 | {"account", primary.account}}; | |
ad528718 | 1832 | doc.emplace_back(obj); |
241dc035 AT |
1833 | } |
1834 | resp->setJsonBody(doc); | |
1835 | } | |
782e9b24 | 1836 | |
212f57b8 FM |
1837 | static void apiServerAutoprimariesPOST(HttpRequest* req, HttpResponse* resp) |
1838 | { | |
efc47b04 | 1839 | UeberBackend backend; |
782e9b24 | 1840 | |
241dc035 AT |
1841 | const auto& document = req->json(); |
1842 | ||
1843 | AutoPrimary primary(stringFromJson(document, "ip"), stringFromJson(document, "nameserver"), ""); | |
1844 | ||
1845 | if (document["account"].is_string()) { | |
1846 | primary.account = document["account"].string_value(); | |
782e9b24 | 1847 | } |
241dc035 AT |
1848 | |
1849 | if (primary.ip.empty() or primary.nameserver.empty()) { | |
1850 | throw ApiException("ip and nameserver fields must be filled"); | |
1851 | } | |
efc47b04 | 1852 | if (!backend.autoPrimaryAdd(primary)) { |
241dc035 AT |
1853 | throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature"); |
1854 | } | |
1855 | resp->body = ""; | |
1856 | resp->status = 201; | |
1857 | } | |
1858 | ||
b9ecbd3b | 1859 | // create new zone |
212f57b8 FM |
1860 | static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp) |
1861 | { | |
efc47b04 FM |
1862 | UeberBackend backend; |
1863 | DNSSECKeeper dnssecKeeper(&backend); | |
1864 | DomainInfo domainInfo; | |
fb9be461 | 1865 | const auto& document = req->json(); |
b9ecbd3b CH |
1866 | DNSName zonename = apiNameToDNSName(stringFromJson(document, "name")); |
1867 | apiCheckNameAllowedCharacters(zonename.toString()); | |
1868 | zonename.makeUsLowerCase(); | |
4ebf78b1 | 1869 | |
efc47b04 | 1870 | bool exists = backend.getDomainInfo(zonename, domainInfo); |
66ade8ac | 1871 | if (exists) { |
b9ecbd3b | 1872 | throw HttpConflictException(); |
66ade8ac | 1873 | } |
e2dba705 | 1874 | |
8db3d7a6 | 1875 | boost::optional<DomainInfo::DomainKind> kind; |
d525b58b | 1876 | boost::optional<vector<ComboAddress>> primaries; |
8db3d7a6 CH |
1877 | boost::optional<DNSName> catalog; |
1878 | boost::optional<string> account; | |
d525b58b | 1879 | extractDomainInfoFromDocument(document, kind, primaries, catalog, account); |
8db3d7a6 | 1880 | |
b9ecbd3b | 1881 | // validate 'kind' is set |
8db3d7a6 CH |
1882 | if (!kind) { |
1883 | throw JsonException("Key 'kind' not present or not a String"); | |
1884 | } | |
1885 | DomainInfo::DomainKind zonekind = *kind; | |
bb9fd223 | 1886 | |
b9ecbd3b CH |
1887 | string zonestring = document["zone"].string_value(); |
1888 | auto rrsets = document["rrsets"]; | |
66ade8ac | 1889 | if (rrsets.is_array() && !zonestring.empty()) { |
b9ecbd3b | 1890 | throw ApiException("You cannot give rrsets AND zone data as text"); |
66ade8ac | 1891 | } |
0f0e73fe | 1892 | |
21b4feba | 1893 | const auto& nameservers = document["nameservers"]; |
c02c999b | 1894 | if (!nameservers.is_null() && !nameservers.is_array() && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) { |
b9ecbd3b | 1895 | throw ApiException("Nameservers is not a list"); |
66ade8ac | 1896 | } |
e2dba705 | 1897 | |
b9ecbd3b CH |
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; | |
0f0e73fe | 1903 | |
b9ecbd3b CH |
1904 | try { |
1905 | if (rrsets.is_array()) { | |
1906 | for (const auto& rrset : rrsets.array_items()) { | |
1907 | DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name")); | |
1908 | apiCheckQNameAllowedCharacters(qname.toString()); | |
1909 | QType qtype; | |
1910 | qtype = stringFromJson(rrset, "type"); | |
1911 | if (qtype.getCode() == 0) { | |
212f57b8 | 1912 | throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given"); |
b9ecbd3b CH |
1913 | } |
1914 | if (rrset["records"].is_array()) { | |
83c8688a | 1915 | uint32_t ttl = uintFromJson(rrset, "ttl"); |
b9ecbd3b CH |
1916 | gatherRecords(rrset, qname, qtype, ttl, new_records); |
1917 | } | |
1918 | if (rrset["comments"].is_array()) { | |
1919 | gatherComments(rrset, qname, qtype, new_comments); | |
6754ef71 CH |
1920 | } |
1921 | } | |
212f57b8 FM |
1922 | } |
1923 | else if (!zonestring.empty()) { | |
b9ecbd3b | 1924 | gatherRecordsFromZone(zonestring, new_records, zonename); |
cf17e6b8 | 1925 | } |
b9ecbd3b | 1926 | } |
21b4feba AT |
1927 | catch (const JsonException& exc) { |
1928 | throw ApiException("New RRsets are invalid: " + string(exc.what())); | |
b9ecbd3b | 1929 | } |
0f0e73fe | 1930 | |
8db3d7a6 CH |
1931 | if (zonekind == DomainInfo::Consumer && !new_records.empty()) { |
1932 | throw ApiException("Zone data MUST NOT be given for Consumer zones"); | |
1933 | } | |
1934 | ||
efc47b04 FM |
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"); | |
66ade8ac CH |
1939 | } |
1940 | ||
efc47b04 | 1941 | apiCheckQNameAllowedCharacters(resourceRecord.qname.toString()); |
f63168e6 | 1942 | |
efc47b04 | 1943 | if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) { |
b9ecbd3b | 1944 | have_soa = true; |
f63168e6 | 1945 | } |
efc47b04 | 1946 | if (resourceRecord.qtype.getCode() == QType::NS && resourceRecord.qname == zonename) { |
b9ecbd3b CH |
1947 | have_zone_ns = true; |
1948 | } | |
1949 | } | |
f7bfeb30 | 1950 | |
b9ecbd3b CH |
1951 | // synthesize RRs as needed |
1952 | DNSResourceRecord autorr; | |
1953 | autorr.qname = zonename; | |
1954 | autorr.auth = true; | |
1955 | autorr.ttl = ::arg().asNum("default-ttl"); | |
e2dba705 | 1956 | |
c02c999b | 1957 | if (!have_soa && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) { |
b9ecbd3b CH |
1958 | // synthesize a SOA record so the zone "really" exists |
1959 | string soa = ::arg()["default-soa-content"]; | |
1960 | boost::replace_all(soa, "@", zonename.toStringNoDot()); | |
efc47b04 FM |
1961 | SOAData soaData; |
1962 | fillSOAData(soa, soaData); | |
1963 | soaData.serial = document["serial"].int_value(); | |
b9ecbd3b | 1964 | autorr.qtype = QType::SOA; |
efc47b04 | 1965 | autorr.content = makeSOAContent(soaData)->getZoneRepresentation(true); |
b9ecbd3b CH |
1966 | // updateDomainSettingsFromDocument will apply SOA-EDIT-API as needed |
1967 | new_records.push_back(autorr); | |
1968 | } | |
1969 | ||
1970 | // create NS records if nameservers are given | |
1971 | for (const auto& value : nameservers.array_items()) { | |
1972 | const string& nameserver = value.string_value(); | |
66ade8ac | 1973 | if (nameserver.empty()) { |
b9ecbd3b | 1974 | throw ApiException("Nameservers must be non-empty strings"); |
66ade8ac | 1975 | } |
8db3d7a6 CH |
1976 | if (zonekind == DomainInfo::Consumer) { |
1977 | throw ApiException("Nameservers MUST NOT be given for Consumer zones"); | |
1978 | } | |
66ade8ac | 1979 | if (!isCanonical(nameserver)) { |
b9ecbd3b | 1980 | throw ApiException("Nameserver is not canonical: '" + nameserver + "'"); |
66ade8ac | 1981 | } |
b9ecbd3b CH |
1982 | try { |
1983 | // ensure the name parses | |
1984 | autorr.content = DNSName(nameserver).toStringRootDot(); | |
212f57b8 FM |
1985 | } |
1986 | catch (...) { | |
b9ecbd3b CH |
1987 | throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'"); |
1988 | } | |
1989 | autorr.qtype = QType::NS; | |
1990 | new_records.push_back(autorr); | |
1991 | if (have_zone_ns) { | |
1992 | throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets"); | |
e2dba705 | 1993 | } |
b9ecbd3b | 1994 | } |
e2dba705 | 1995 | |
b9ecbd3b | 1996 | checkNewRecords(new_records, zonename); |
e3675a8a | 1997 | |
b9ecbd3b CH |
1998 | if (boolFromJson(document, "dnssec", false)) { |
1999 | checkDefaultDNSSECAlgos(); | |
986e4858 | 2000 | |
212f57b8 | 2001 | if (document["nsec3param"].string_value().length() > 0) { |
b9ecbd3b | 2002 | NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value()); |
0e3e5d0b | 2003 | string error_msg; |
efc47b04 | 2004 | if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) { |
212f57b8 | 2005 | throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg); |
986e4858 PL |
2006 | } |
2007 | } | |
b9ecbd3b | 2008 | } |
986e4858 | 2009 | |
b9ecbd3b | 2010 | // no going back after this |
efc47b04 | 2011 | if (!backend.createDomain(zonename, kind.get_value_or(DomainInfo::Native), primaries.get_value_or(vector<ComboAddress>()), account.get_value_or(""))) { |
212f57b8 | 2012 | throw ApiException("Creating domain '" + zonename.toString() + "' failed: backend refused"); |
66ade8ac | 2013 | } |
f63168e6 | 2014 | |
efc47b04 | 2015 | if (!backend.getDomainInfo(zonename, domainInfo)) { |
212f57b8 | 2016 | throw ApiException("Creating domain '" + zonename.toString() + "' failed: lookup of domain ID failed"); |
66ade8ac | 2017 | } |
f63168e6 | 2018 | |
efc47b04 | 2019 | domainInfo.backend->startTransaction(zonename, static_cast<int>(domainInfo.id)); |
f43646f5 | 2020 | |
b9ecbd3b | 2021 | // will be overridden by updateDomainSettingsFromDocument, if given in document. |
efc47b04 | 2022 | domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", "DEFAULT"); |
9440a9f0 | 2023 | |
efc47b04 FM |
2024 | for (auto& resourceRecord : new_records) { |
2025 | resourceRecord.domain_id = static_cast<int>(domainInfo.id); | |
2026 | domainInfo.backend->feedRecord(resourceRecord, DNSName()); | |
b9ecbd3b | 2027 | } |
212f57b8 | 2028 | for (Comment& comment : new_comments) { |
efc47b04 FM |
2029 | comment.domain_id = static_cast<int>(domainInfo.id); |
2030 | if (!domainInfo.backend->feedComment(comment)) { | |
b9ecbd3b | 2031 | throw ApiException("Hosting backend does not support editing comments."); |
f63168e6 | 2032 | } |
b9ecbd3b | 2033 | } |
e2dba705 | 2034 | |
efc47b04 | 2035 | updateDomainSettingsFromDocument(backend, domainInfo, zonename, document, !new_records.empty()); |
f63168e6 | 2036 | |
d525b58b | 2037 | if (!catalog && kind == DomainInfo::Primary) { |
3b45a434 | 2038 | const auto& defaultCatalog = ::arg()["default-catalog-zone"]; |
c8734ecd | 2039 | if (!defaultCatalog.empty()) { |
efc47b04 | 2040 | domainInfo.backend->setCatalog(zonename, DNSName(defaultCatalog)); |
c8734ecd PD |
2041 | } |
2042 | } | |
2043 | ||
efc47b04 | 2044 | domainInfo.backend->commitTransaction(); |
f4922e19 | 2045 | |
efc47b04 | 2046 | g_zoneCache.add(zonename, static_cast<int>(domainInfo.id)); // make new zone visible |
e2dba705 | 2047 | |
efc47b04 | 2048 | fillZone(backend, zonename, resp, req); |
b9ecbd3b CH |
2049 | resp->status = 201; |
2050 | } | |
c67bf8c5 | 2051 | |
b9ecbd3b | 2052 | // list known zones |
212f57b8 FM |
2053 | static void apiServerZonesGET(HttpRequest* req, HttpResponse* resp) |
2054 | { | |
efc47b04 FM |
2055 | UeberBackend backend; |
2056 | DNSSECKeeper dnssecKeeper(&backend); | |
c67bf8c5 | 2057 | vector<DomainInfo> domains; |
e543cc8f | 2058 | |
ad528718 | 2059 | if (req->getvars.count("zone") != 0) { |
37e01df4 | 2060 | string zone = req->getvars["zone"]; |
e543cc8f CH |
2061 | apiCheckNameAllowedCharacters(zone); |
2062 | DNSName zonename = apiNameToDNSName(zone); | |
2063 | zonename.makeUsLowerCase(); | |
efc47b04 FM |
2064 | DomainInfo domainInfo; |
2065 | if (backend.getDomainInfo(zonename, domainInfo)) { | |
2066 | domains.push_back(domainInfo); | |
e543cc8f | 2067 | } |
212f57b8 FM |
2068 | } |
2069 | else { | |
72abd9ef | 2070 | try { |
efc47b04 | 2071 | backend.getAllDomains(&domains, true, true); // incl. serial and disabled |
212f57b8 | 2072 | } |
efc47b04 FM |
2073 | catch (const PDNSException& exception) { |
2074 | throw HttpInternalServerErrorException("Could not retrieve all domain information: " + exception.reason); | |
72abd9ef | 2075 | } |
e543cc8f | 2076 | } |
c67bf8c5 | 2077 | |
07a32d18 | 2078 | bool with_dnssec = true; |
ad528718 | 2079 | if (req->getvars.count("dnssec") != 0) { |
07a32d18 CH |
2080 | // can send ?dnssec=false to improve performance. |
2081 | string dnssec_flag = req->getvars["dnssec"]; | |
2082 | if (dnssec_flag == "false") { | |
2083 | with_dnssec = false; | |
2084 | } | |
2085 | } | |
2086 | ||
62a9a74c | 2087 | Json::array doc; |
c8b929d9 | 2088 | doc.reserve(domains.size()); |
ad528718 | 2089 | for (const DomainInfo& domainInfo : domains) { |
efc47b04 | 2090 | doc.emplace_back(getZoneInfo(domainInfo, with_dnssec ? &dnssecKeeper : nullptr)); |
c67bf8c5 | 2091 | } |
917f686a | 2092 | resp->setJsonBody(doc); |
c67bf8c5 CH |
2093 | } |
2094 | ||
212f57b8 FM |
2095 | static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp) |
2096 | { | |
7330f904 | 2097 | ZoneData zoneData{req}; |
77bfe8de | 2098 | |
b62707f9 AT |
2099 | // update domain contents and/or settings |
2100 | const auto& document = req->json(); | |
7c0ba3d2 | 2101 | |
b62707f9 AT |
2102 | auto rrsets = document["rrsets"]; |
2103 | bool zoneWasModified = false; | |
7330f904 | 2104 | DomainInfo::DomainKind newKind = zoneData.domainInfo.kind; |
b62707f9 AT |
2105 | if (document["kind"].is_string()) { |
2106 | newKind = DomainInfo::stringToKind(stringFromJson(document, "kind")); | |
2107 | } | |
26636b05 | 2108 | |
b62707f9 AT |
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; | |
2114 | string soaEditKind; | |
7330f904 FM |
2115 | zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT-API", soaEditApiKind); |
2116 | zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT", soaEditKind); | |
70f1db7c | 2117 | |
b62707f9 AT |
2118 | vector<DNSResourceRecord> new_records; |
2119 | vector<Comment> new_comments; | |
70f1db7c | 2120 | |
b62707f9 AT |
2121 | try { |
2122 | for (const auto& rrset : rrsets.array_items()) { | |
2123 | DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name")); | |
2124 | apiCheckQNameAllowedCharacters(qname.toString()); | |
2125 | QType qtype; | |
2126 | qtype = stringFromJson(rrset, "type"); | |
2127 | if (qtype.getCode() == 0) { | |
212f57b8 | 2128 | throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given"); |
70f1db7c | 2129 | } |
b62707f9 AT |
2130 | if (rrset["records"].is_array()) { |
2131 | uint32_t ttl = uintFromJson(rrset, "ttl"); | |
2132 | gatherRecords(rrset, qname, qtype, ttl, new_records); | |
66ade8ac | 2133 | } |
b62707f9 AT |
2134 | if (rrset["comments"].is_array()) { |
2135 | gatherComments(rrset, qname, qtype, new_comments); | |
70f1db7c CH |
2136 | } |
2137 | } | |
b62707f9 AT |
2138 | } |
2139 | catch (const JsonException& exc) { | |
2140 | throw ApiException("New RRsets are invalid: " + string(exc.what())); | |
2141 | } | |
70f1db7c | 2142 | |
efc47b04 FM |
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"); | |
8db3d7a6 | 2147 | } |
efc47b04 | 2148 | apiCheckQNameAllowedCharacters(resourceRecord.qname.toString()); |
70f1db7c | 2149 | |
efc47b04 | 2150 | if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zoneData.zoneName) { |
b62707f9 | 2151 | haveSoa = true; |
70f1db7c | 2152 | } |
b62707f9 | 2153 | } |
26636b05 | 2154 | |
b62707f9 AT |
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"); | |
2158 | } | |
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"); | |
70f1db7c CH |
2162 | } |
2163 | ||
7330f904 | 2164 | checkNewRecords(new_records, zoneData.zoneName); |
7c0ba3d2 | 2165 | |
7330f904 | 2166 | zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id)); |
efc47b04 FM |
2167 | for (auto& resourceRecord : new_records) { |
2168 | resourceRecord.domain_id = static_cast<int>(zoneData.domainInfo.id); | |
2169 | zoneData.domainInfo.backend->feedRecord(resourceRecord, DNSName()); | |
b62707f9 | 2170 | } |
212f57b8 | 2171 | for (Comment& comment : new_comments) { |
7330f904 FM |
2172 | comment.domain_id = static_cast<int>(zoneData.domainInfo.id); |
2173 | zoneData.domainInfo.backend->feedComment(comment); | |
b62707f9 | 2174 | } |
db82ebde | 2175 | |
b62707f9 | 2176 | if (!haveSoa && (newKind == DomainInfo::Secondary || newKind == DomainInfo::Consumer)) { |
7330f904 | 2177 | zoneData.domainInfo.backend->setStale(zoneData.domainInfo.id); |
b62707f9 | 2178 | } |
212f57b8 FM |
2179 | } |
2180 | else { | |
b62707f9 | 2181 | // avoid deleting current zone contents |
7330f904 | 2182 | zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1); |
7c0ba3d2 | 2183 | } |
a44d979f | 2184 | |
b62707f9 | 2185 | // updateDomainSettingsFromDocument will rectify the zone and update SOA serial. |
7330f904 FM |
2186 | updateDomainSettingsFromDocument(zoneData.backend, zoneData.domainInfo, zoneData.zoneName, document, zoneWasModified); |
2187 | zoneData.domainInfo.backend->commitTransaction(); | |
a44d979f | 2188 | |
7330f904 | 2189 | purgeAuthCaches(zoneData.zoneName.toString() + "$"); |
b72b74c5 | 2190 | |
b62707f9 AT |
2191 | resp->body = ""; |
2192 | resp->status = 204; // No Content, but indicate success | |
2193 | } | |
2194 | ||
212f57b8 FM |
2195 | static void apiServerZoneDetailDELETE(HttpRequest* req, HttpResponse* resp) |
2196 | { | |
7330f904 | 2197 | ZoneData zoneData{req}; |
b62707f9 AT |
2198 | |
2199 | // delete domain | |
2200 | ||
7330f904 | 2201 | zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1); |
b62707f9 | 2202 | try { |
7330f904 FM |
2203 | if (!zoneData.domainInfo.backend->deleteDomain(zoneData.zoneName)) { |
2204 | throw ApiException("Deleting domain '" + zoneData.zoneName.toString() + "' failed: backend delete failed/unsupported"); | |
a44d979f | 2205 | } |
a462a01d | 2206 | |
7330f904 | 2207 | zoneData.domainInfo.backend->commitTransaction(); |
5a4fa722 | 2208 | |
7330f904 | 2209 | g_zoneCache.remove(zoneData.zoneName); |
212f57b8 FM |
2210 | } |
2211 | catch (...) { | |
7330f904 | 2212 | zoneData.domainInfo.backend->abortTransaction(); |
b62707f9 | 2213 | throw; |
a462a01d | 2214 | } |
b62707f9 AT |
2215 | |
2216 | // clear caches | |
7330f904 FM |
2217 | DNSSECKeeper::clearCaches(zoneData.zoneName); |
2218 | purgeAuthCaches(zoneData.zoneName.toString() + "$"); | |
b62707f9 AT |
2219 | |
2220 | // empty body on success | |
2221 | resp->body = ""; | |
2222 | resp->status = 204; // No Content: declare that the zone is gone now | |
2223 | } | |
2224 | ||
212f57b8 FM |
2225 | static void apiServerZoneDetailPATCH(HttpRequest* req, HttpResponse* resp) |
2226 | { | |
7330f904 FM |
2227 | ZoneData zoneData{req}; |
2228 | patchZone(zoneData.backend, zoneData.zoneName, zoneData.domainInfo, req, resp); | |
b62707f9 AT |
2229 | } |
2230 | ||
212f57b8 FM |
2231 | static void apiServerZoneDetailGET(HttpRequest* req, HttpResponse* resp) |
2232 | { | |
7330f904 FM |
2233 | ZoneData zoneData{req}; |
2234 | fillZone(zoneData.backend, zoneData.zoneName, resp, req); | |
b62707f9 AT |
2235 | } |
2236 | ||
212f57b8 FM |
2237 | static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) |
2238 | { | |
7330f904 | 2239 | ZoneData zoneData{req}; |
a83004d3 | 2240 | |
ad528718 | 2241 | ostringstream outputStringStream; |
a83004d3 | 2242 | |
ad528718 FM |
2243 | DNSResourceRecord resourceRecord; |
2244 | SOAData soaData; | |
7330f904 FM |
2245 | zoneData.domainInfo.backend->list(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id)); |
2246 | while (zoneData.domainInfo.backend->get(resourceRecord)) { | |
ad528718 | 2247 | if (resourceRecord.qtype.getCode() == 0) { |
a83004d3 | 2248 | continue; // skip empty non-terminals |
ad528718 | 2249 | } |
a83004d3 | 2250 | |
ad528718 FM |
2251 | outputStringStream << resourceRecord.qname.toString() << "\t" << resourceRecord.ttl << "\t" |
2252 | << "IN" | |
2253 | << "\t" << resourceRecord.qtype.toString() << "\t" << makeApiRecordContent(resourceRecord.qtype, resourceRecord.content) << endl; | |
a83004d3 CH |
2254 | } |
2255 | ||
2256 | if (req->accept_json) { | |
ad528718 | 2257 | resp->setJsonBody(Json::object{{"zone", outputStringStream.str()}}); |
212f57b8 FM |
2258 | } |
2259 | else { | |
a83004d3 | 2260 | resp->headers["Content-Type"] = "text/plain; charset=us-ascii"; |
ad528718 | 2261 | resp->body = outputStringStream.str(); |
a83004d3 CH |
2262 | } |
2263 | } | |
2264 | ||
212f57b8 FM |
2265 | static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) |
2266 | { | |
7330f904 | 2267 | ZoneData zoneData{req}; |
a426cb89 | 2268 | |
7330f904 FM |
2269 | if (zoneData.domainInfo.primaries.empty()) { |
2270 | throw ApiException("Domain '" + zoneData.zoneName.toString() + "' is not a secondary domain (or has no primary defined)"); | |
ad528718 | 2271 | } |
a426cb89 | 2272 | |
7330f904 FM |
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()); | |
a426cb89 CH |
2276 | } |
2277 | ||
212f57b8 FM |
2278 | static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) |
2279 | { | |
7330f904 | 2280 | ZoneData zoneData{req}; |
a426cb89 | 2281 | |
7330f904 | 2282 | if (!Communicator.notifyDomain(zoneData.zoneName, &zoneData.backend)) { |
a426cb89 | 2283 | throw ApiException("Failed to add to the queue - see server log"); |
ad528718 | 2284 | } |
a426cb89 | 2285 | |
692829aa | 2286 | resp->setSuccessResult("Notification queued"); |
a426cb89 CH |
2287 | } |
2288 | ||
212f57b8 FM |
2289 | static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp) |
2290 | { | |
7330f904 | 2291 | ZoneData zoneData{req}; |
4bc8379e | 2292 | |
7330f904 FM |
2293 | if (zoneData.dnssecKeeper.isPresigned(zoneData.zoneName)) { |
2294 | throw ApiException("Zone '" + zoneData.zoneName.toString() + "' is pre-signed, not rectifying."); | |
ad528718 | 2295 | } |
4bc8379e | 2296 | |
ad528718 | 2297 | string error_msg; |
59102608 | 2298 | string info; |
7330f904 FM |
2299 | if (!zoneData.dnssecKeeper.rectifyZone(zoneData.zoneName, error_msg, info, true)) { |
2300 | throw ApiException("Failed to rectify '" + zoneData.zoneName.toString() + "' " + error_msg); | |
ad528718 | 2301 | } |
4bc8379e PL |
2302 | |
2303 | resp->setSuccessResult("Rectified"); | |
2304 | } | |
2305 | ||
efc47b04 FM |
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) | |
994a94ce | 2308 | { |
ad528718 FM |
2309 | bool zone_disabled = false; |
2310 | SOAData soaData; | |
b3905a3d | 2311 | |
f63168e6 CH |
2312 | vector<DNSResourceRecord> new_records; |
2313 | vector<Comment> new_comments; | |
d708640f CH |
2314 | vector<DNSResourceRecord> new_ptrs; |
2315 | ||
1f68b185 | 2316 | Json document = req->json(); |
b3905a3d | 2317 | |
1f68b185 | 2318 | auto rrsets = document["rrsets"]; |
ad528718 | 2319 | if (!rrsets.is_array()) { |
d708640f | 2320 | throw ApiException("No rrsets given in update request"); |
ad528718 | 2321 | } |
b3905a3d | 2322 | |
efc47b04 | 2323 | domainInfo.backend->startTransaction(zonename); |
6cc98ddf | 2324 | |
d708640f | 2325 | try { |
d29d5db7 | 2326 | string soa_edit_api_kind; |
a6448d95 | 2327 | string soa_edit_kind; |
efc47b04 FM |
2328 | domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind); |
2329 | domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind); | |
d29d5db7 CH |
2330 | bool soa_edit_done = false; |
2331 | ||
905dae56 | 2332 | set<std::tuple<DNSName, QType, string>> seen; |
2b048cc7 | 2333 | |
6754ef71 CH |
2334 | for (const auto& rrset : rrsets.array_items()) { |
2335 | string changetype = toUpper(stringFromJson(rrset, "changetype")); | |
c576d0c5 | 2336 | DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name")); |
cb9b5901 | 2337 | apiCheckQNameAllowedCharacters(qname.toString()); |
6754ef71 | 2338 | QType qtype; |
d708640f | 2339 | qtype = stringFromJson(rrset, "type"); |
6754ef71 | 2340 | if (qtype.getCode() == 0) { |
212f57b8 | 2341 | throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given"); |
6754ef71 | 2342 | } |
d708640f | 2343 | |
ad528718 | 2344 | if (seen.count({qname, qtype, changetype}) != 0) { |
212f57b8 | 2345 | throw ApiException("Duplicate RRset " + qname.toString() + " IN " + qtype.toString() + " with changetype: " + changetype); |
2b048cc7 | 2346 | } |
e732e9cc | 2347 | seen.insert({qname, qtype, changetype}); |
d708640f | 2348 | |
d708640f | 2349 | if (changetype == "DELETE") { |
b7f21ab1 | 2350 | // delete all matching qname/qtype RRs (and, implicitly comments). |
efc47b04 | 2351 | if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, vector<DNSResourceRecord>())) { |
d708640f | 2352 | throw ApiException("Hosting backend does not support editing records."); |
6cc98ddf | 2353 | } |
d708640f CH |
2354 | } |
2355 | else if (changetype == "REPLACE") { | |
1d6b70f9 | 2356 | // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records. |
ad528718 | 2357 | if (!qname.isPartOf(zonename) && qname != zonename) { |
212f57b8 | 2358 | throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Name is out of zone"); |
ad528718 | 2359 | } |
34df6ecc | 2360 | |
6754ef71 CH |
2361 | bool replace_records = rrset["records"].is_array(); |
2362 | bool replace_comments = rrset["comments"].is_array(); | |
f63168e6 | 2363 | |
6754ef71 | 2364 | if (!replace_records && !replace_comments) { |
d5fcd583 | 2365 | throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.toString()); |
6754ef71 | 2366 | } |
f63168e6 | 2367 | |
6754ef71 CH |
2368 | new_records.clear(); |
2369 | new_comments.clear(); | |
f63168e6 | 2370 | |
cf17e6b8 CH |
2371 | try { |
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. | |
83c8688a | 2374 | uint32_t ttl = uintFromJson(rrset, "ttl"); |
cf17e6b8 CH |
2375 | gatherRecords(rrset, qname, qtype, ttl, new_records); |
2376 | ||
ad528718 | 2377 | for (DNSResourceRecord& resourceRecord : new_records) { |
efc47b04 | 2378 | resourceRecord.domain_id = static_cast<int>(domainInfo.id); |
ad528718 FM |
2379 | if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) { |
2380 | soa_edit_done = increaseSOARecord(resourceRecord, soa_edit_api_kind, soa_edit_kind); | |
cf17e6b8 | 2381 | } |
6754ef71 | 2382 | } |
cf17e6b8 | 2383 | checkNewRecords(new_records, zonename); |
d708640f | 2384 | } |
6cc98ddf | 2385 | |
cf17e6b8 CH |
2386 | if (replace_comments) { |
2387 | gatherComments(rrset, qname, qtype, new_comments); | |
f63168e6 | 2388 | |
ad528718 | 2389 | for (Comment& comment : new_comments) { |
efc47b04 | 2390 | comment.domain_id = static_cast<int>(domainInfo.id); |
cf17e6b8 | 2391 | } |
6754ef71 | 2392 | } |
d708640f | 2393 | } |
cf17e6b8 CH |
2394 | catch (const JsonException& e) { |
2395 | throw ApiException("New RRsets are invalid: " + string(e.what())); | |
2396 | } | |
b3905a3d | 2397 | |
d708640f | 2398 | if (replace_records) { |
03b1cc25 | 2399 | bool ent_present = false; |
ad528718 FM |
2400 | bool dname_seen = false; |
2401 | bool ns_seen = false; | |
5e73d266 | 2402 | |
efc47b04 | 2403 | domainInfo.backend->lookup(QType(QType::ANY), qname, static_cast<int>(domainInfo.id)); |
ad528718 | 2404 | DNSResourceRecord resourceRecord; |
efc47b04 | 2405 | while (domainInfo.backend->get(resourceRecord)) { |
ad528718 | 2406 | if (resourceRecord.qtype.getCode() == QType::ENT) { |
03b1cc25 | 2407 | ent_present = true; |
002c4fe1 RG |
2408 | /* that's fine, we will override it */ |
2409 | continue; | |
03b1cc25 | 2410 | } |
ad528718 | 2411 | if (qtype == QType::DNAME || resourceRecord.qtype == QType::DNAME) { |
5e73d266 | 2412 | dname_seen = true; |
ad528718 FM |
2413 | } |
2414 | if (qtype == QType::NS || resourceRecord.qtype == QType::NS) { | |
5e73d266 | 2415 | ns_seen = true; |
ad528718 FM |
2416 | } |
2417 | if (qtype.getCode() != resourceRecord.qtype.getCode() | |
212f57b8 | 2418 | && (exclusiveEntryTypes.count(qtype.getCode()) != 0 |
ad528718 | 2419 | || exclusiveEntryTypes.count(resourceRecord.qtype.getCode()) != 0)) { |
e4e8ca30 PD |
2420 | |
2421 | // leave database handle in a consistent state | |
efc47b04 | 2422 | while (domainInfo.backend->get(resourceRecord)) { |
e4e8ca30 | 2423 | ; |
ad528718 | 2424 | } |
e4e8ca30 | 2425 | |
212f57b8 | 2426 | throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Conflicts with pre-existing RRset"); |
8560f36a CH |
2427 | } |
2428 | } | |
2429 | ||
5e73d266 | 2430 | if (dname_seen && ns_seen && qname != zonename) { |
212f57b8 | 2431 | throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Cannot have both NS and DNAME except in zone apex"); |
5e73d266 | 2432 | } |
efc47b04 | 2433 | if (!new_records.empty() && domainInfo.kind == DomainInfo::Consumer) { |
8db3d7a6 CH |
2434 | // Allow deleting all RRsets, just not modifying them. |
2435 | throw ApiException("Modifying RRsets in Consumer zones is unsupported"); | |
2436 | } | |
03b1cc25 CH |
2437 | if (!new_records.empty() && ent_present) { |
2438 | QType qt_ent{0}; | |
efc47b04 | 2439 | if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qt_ent, new_records)) { |
03b1cc25 CH |
2440 | throw ApiException("Hosting backend does not support editing records."); |
2441 | } | |
2442 | } | |
efc47b04 | 2443 | if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, new_records)) { |
d708640f CH |
2444 | throw ApiException("Hosting backend does not support editing records."); |
2445 | } | |
2446 | } | |
2447 | if (replace_comments) { | |
efc47b04 | 2448 | if (!domainInfo.backend->replaceComments(domainInfo.id, qname, qtype, new_comments)) { |
d708640f CH |
2449 | throw ApiException("Hosting backend does not support editing comments."); |
2450 | } | |
2451 | } | |
6cc98ddf | 2452 | } |
ad528718 | 2453 | else { |
d708640f | 2454 | throw ApiException("Changetype not understood"); |
ad528718 | 2455 | } |
6cc98ddf | 2456 | } |
d29d5db7 | 2457 | |
efc47b04 | 2458 | zone_disabled = (!backend.getSOAUncached(zonename, soaData)); |
d29d5db7 | 2459 | |
a41a789a KM |
2460 | // edit SOA (if needed) |
2461 | if (!zone_disabled && !soa_edit_api_kind.empty() && !soa_edit_done) { | |
ad528718 FM |
2462 | DNSResourceRecord resourceRecord; |
2463 | if (makeIncreasedSOARecord(soaData, soa_edit_api_kind, soa_edit_kind, resourceRecord)) { | |
efc47b04 | 2464 | if (!domainInfo.backend->replaceRRSet(domainInfo.id, resourceRecord.qname, resourceRecord.qtype, vector<DNSResourceRecord>(1, resourceRecord))) { |
13f9e280 CH |
2465 | throw ApiException("Hosting backend does not support editing records."); |
2466 | } | |
d29d5db7 | 2467 | } |
3ae63ca8 | 2468 | |
478de03b | 2469 | // return old and new serials in headers |
ad528718 FM |
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); | |
d29d5db7 | 2473 | } |
212f57b8 FM |
2474 | } |
2475 | catch (...) { | |
efc47b04 | 2476 | domainInfo.backend->abortTransaction(); |
d708640f CH |
2477 | throw; |
2478 | } | |
986e4858 | 2479 | |
0ca59ad9 | 2480 | // Rectify |
efc47b04 FM |
2481 | DNSSECKeeper dnssecKeeper(&backend); |
2482 | if (!zone_disabled && !dnssecKeeper.isPresigned(zonename) && isZoneApiRectifyEnabled(domainInfo)) { | |
d71d2c88 CH |
2483 | string info; |
2484 | string error_msg; | |
ad528718 | 2485 | if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false)) { |
d71d2c88 | 2486 | throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg); |
0ca59ad9 | 2487 | } |
986e4858 PL |
2488 | } |
2489 | ||
efc47b04 | 2490 | domainInfo.backend->commitTransaction(); |
b3905a3d | 2491 | |
cbc5c674 | 2492 | DNSSECKeeper::clearCaches(zonename); |
27e14380 | 2493 | purgeAuthCaches(zonename.toString() + "$"); |
d1587ceb | 2494 | |
f0e76cee CH |
2495 | resp->body = ""; |
2496 | resp->status = 204; // No Content, but indicate success | |
b3905a3d CH |
2497 | } |
2498 | ||
212f57b8 FM |
2499 | static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) |
2500 | { | |
ad528718 FM |
2501 | string qVar = req->getvars["q"]; |
2502 | string sMaxVar = req->getvars["max"]; | |
2503 | string sObjectTypeVar = req->getvars["object_type"]; | |
45250285 | 2504 | |
fd9af6ec FM |
2505 | size_t maxEnts = 100; |
2506 | size_t ents = 0; | |
720ed2bd | 2507 | |
45250285 | 2508 | // the following types of data can be searched for using the api |
0bd91de6 | 2509 | enum class ObjectType |
45250285 JE |
2510 | { |
2511 | ALL, | |
2512 | ZONE, | |
2513 | RECORD, | |
2514 | COMMENT | |
ad528718 | 2515 | } objectType{}; |
45250285 | 2516 | |
ad528718 | 2517 | if (qVar.empty()) { |
b1902fab | 2518 | throw ApiException("Query q can't be blank"); |
ad528718 FM |
2519 | } |
2520 | if (!sMaxVar.empty()) { | |
2521 | maxEnts = std::stoi(sMaxVar); | |
2522 | } | |
2523 | if (maxEnts < 1) { | |
720ed2bd | 2524 | throw ApiException("Maximum entries must be larger than 0"); |
ad528718 | 2525 | } |
b1902fab | 2526 | |
ad528718 | 2527 | if (sObjectTypeVar.empty() || sObjectTypeVar == "all") { |
0bd91de6 | 2528 | objectType = ObjectType::ALL; |
ad528718 FM |
2529 | } |
2530 | else if (sObjectTypeVar == "zone") { | |
0bd91de6 | 2531 | objectType = ObjectType::ZONE; |
ad528718 FM |
2532 | } |
2533 | else if (sObjectTypeVar == "record") { | |
0bd91de6 | 2534 | objectType = ObjectType::RECORD; |
ad528718 FM |
2535 | } |
2536 | else if (sObjectTypeVar == "comment") { | |
0bd91de6 | 2537 | objectType = ObjectType::COMMENT; |
ad528718 FM |
2538 | } |
2539 | else { | |
0bd91de6 | 2540 | throw ApiException("object_type must be one of the following options: all, zone, record, comment"); |
ad528718 | 2541 | } |
45250285 | 2542 | |
ad528718 FM |
2543 | SimpleMatch simpleMatch(qVar, true); |
2544 | UeberBackend backend; | |
b1902fab | 2545 | vector<DomainInfo> domains; |
720ed2bd AT |
2546 | vector<DNSResourceRecord> result_rr; |
2547 | vector<Comment> result_c; | |
212f57b8 FM |
2548 | map<int, DomainInfo> zoneIdZone; |
2549 | map<int, DomainInfo>::iterator val; | |
00963dea | 2550 | Json::array doc; |
b1902fab | 2551 | |
ad528718 | 2552 | backend.getAllDomains(&domains, false, true); |
d2d194a9 | 2553 | |
ad528718 FM |
2554 | for (const DomainInfo& domainInfo : domains) { |
2555 | if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && simpleMatch.match(domainInfo.zone)) { | |
212f57b8 FM |
2556 | doc.push_back(Json::object{ |
2557 | {"object_type", "zone"}, | |
ad528718 FM |
2558 | {"zone_id", apiZoneNameToId(domainInfo.zone)}, |
2559 | {"name", domainInfo.zone.toString()}}); | |
720ed2bd | 2560 | ents++; |
b1902fab | 2561 | } |
ad528718 | 2562 | zoneIdZone[static_cast<int>(domainInfo.id)] = domainInfo; // populate cache |
720ed2bd | 2563 | } |
b1902fab | 2564 | |
ad528718 FM |
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) { | |
7cbc5255 | 2568 | continue; // skip empty non-terminals |
ad528718 | 2569 | } |
7cbc5255 | 2570 | |
212f57b8 FM |
2571 | auto object = Json::object{ |
2572 | {"object_type", "record"}, | |
ad528718 FM |
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)}}; | |
2578 | ||
2579 | val = zoneIdZone.find(resourceRecord.domain_id); | |
2580 | if (val != zoneIdZone.end()) { | |
00963dea CH |
2581 | object["zone_id"] = apiZoneNameToId(val->second.zone); |
2582 | object["zone"] = val->second.zone.toString(); | |
720ed2bd | 2583 | } |
ad528718 | 2584 | doc.emplace_back(object); |
b1902fab | 2585 | } |
720ed2bd | 2586 | } |
b1902fab | 2587 | |
ad528718 FM |
2588 | if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && backend.searchComments(qVar, maxEnts, result_c)) { |
2589 | for (const Comment& comment : result_c) { | |
212f57b8 FM |
2590 | auto object = Json::object{ |
2591 | {"object_type", "comment"}, | |
ad528718 FM |
2592 | {"name", comment.qname.toString()}, |
2593 | {"type", comment.qtype.toString()}, | |
2594 | {"content", comment.content}}; | |
2595 | ||
2596 | val = zoneIdZone.find(comment.domain_id); | |
2597 | if (val != zoneIdZone.end()) { | |
00963dea CH |
2598 | object["zone_id"] = apiZoneNameToId(val->second.zone); |
2599 | object["zone"] = val->second.zone.toString(); | |
720ed2bd | 2600 | } |
ad528718 | 2601 | doc.emplace_back(object); |
b1902fab CH |
2602 | } |
2603 | } | |
4bd3d119 | 2604 | |
917f686a | 2605 | resp->setJsonBody(doc); |
b1902fab CH |
2606 | } |
2607 | ||
212f57b8 FM |
2608 | static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) |
2609 | { | |
c0f6a1da CH |
2610 | DNSName canon = apiNameToDNSName(req->getvars["domain"]); |
2611 | ||
37b89580 | 2612 | if (g_zoneCache.isEnabled()) { |
ad528718 FM |
2613 | DomainInfo domainInfo; |
2614 | UeberBackend backend; | |
2615 | if (backend.getDomainInfo(canon, domainInfo, false)) { | |
37b89580 CH |
2616 | // zone exists (uncached), add/update it in the zone cache. |
2617 | // Handle this first, to avoid concurrent queries re-populating the other caches. | |
ad528718 | 2618 | g_zoneCache.add(domainInfo.zone, static_cast<int>(domainInfo.id)); |
37b89580 | 2619 | } |
b72b74c5 | 2620 | else { |
ad528718 | 2621 | g_zoneCache.remove(domainInfo.zone); |
b72b74c5 | 2622 | } |
37b89580 CH |
2623 | } |
2624 | ||
cbc5c674 | 2625 | DNSSECKeeper::clearCaches(canon); |
7bae5ce4 CH |
2626 | // purge entire zone from cache, not just zone-level records. |
2627 | uint64_t count = purgeAuthCaches(canon.toString() + "$"); | |
212f57b8 FM |
2628 | resp->setJsonBody(Json::object{ |
2629 | {"count", (int)count}, | |
2630 | {"result", "Flushed cache."}}); | |
ddc84d12 CH |
2631 | } |
2632 | ||
ad528718 | 2633 | static std::ostream& operator<<(std::ostream& outStream, StatType statType) |
dec69610 | 2634 | { |
212f57b8 FM |
2635 | switch (statType) { |
2636 | case StatType::counter: | |
ad528718 | 2637 | return outStream << "counter"; |
212f57b8 | 2638 | case StatType::gauge: |
ad528718 | 2639 | return outStream << "gauge"; |
dec69610 | 2640 | }; |
ad528718 | 2641 | return outStream << static_cast<uint16_t>(statType); |
dec69610 MTH |
2642 | } |
2643 | ||
212f57b8 FM |
2644 | static void prometheusMetrics(HttpRequest* /* req */, HttpResponse* resp) |
2645 | { | |
dec69610 | 2646 | std::ostringstream output; |
212f57b8 | 2647 | for (const auto& metricName : S.getEntries()) { |
dec69610 MTH |
2648 | // Prometheus suggest using '_' instead of '-' |
2649 | std::string prometheusMetricName = "pdns_auth_" + boost::replace_all_copy(metricName, "-", "_"); | |
2650 | ||
2651 | output << "# HELP " << prometheusMetricName << " " << S.getDescrip(metricName) << "\n"; | |
2652 | output << "# TYPE " << prometheusMetricName << " " << S.getStatType(metricName) << "\n"; | |
2653 | output << prometheusMetricName << " " << S.read(metricName) << "\n"; | |
2654 | } | |
2655 | ||
212f57b8 FM |
2656 | output << "# HELP pdns_auth_info " |
2657 | << "Info from PowerDNS, value is always 1" | |
2658 | << "\n"; | |
2659 | output << "# TYPE pdns_auth_info " | |
2660 | << "gauge" | |
2661 | << "\n"; | |
2662 | output << "pdns_auth_info{version=\"" << VERSION << "\"} " | |
2663 | << "1" | |
2664 | << "\n"; | |
df04f36b | 2665 | |
dec69610 MTH |
2666 | resp->body = output.str(); |
2667 | resp->headers["Content-Type"] = "text/plain"; | |
2668 | resp->status = 200; | |
2669 | } | |
2670 | ||
ad528718 | 2671 | static void cssfunction(HttpRequest* /* req */, HttpResponse* resp) |
c67bf8c5 | 2672 | { |
80d59cd1 CH |
2673 | resp->headers["Cache-Control"] = "max-age=86400"; |
2674 | resp->headers["Content-Type"] = "text/css"; | |
c67bf8c5 | 2675 | |
1071abdd | 2676 | ostringstream ret; |
212f57b8 FM |
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; | |
80d59cd1 | 2703 | resp->body = ret.str(); |
c146576d | 2704 | resp->status = 200; |
1071abdd CH |
2705 | } |
2706 | ||
dea47634 | 2707 | void AuthWebServer::webThread() |
12c86877 BH |
2708 | { |
2709 | try { | |
519f5484 | 2710 | setThreadName("pdns/webserver"); |
212f57b8 | 2711 | if (::arg().mustDo("api")) { |
a9c69ea5 AT |
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"); | |
c67bf8c5 | 2750 | } |
536ab56f | 2751 | if (::arg().mustDo("webserver")) { |
212f57b8 | 2752 | d_ws->registerWebHandler( |
ad528718 | 2753 | "/style.css", [](HttpRequest* req, HttpResponse* resp) { cssfunction(req, resp); }, "GET"); |
212f57b8 FM |
2754 | d_ws->registerWebHandler( |
2755 | "/", [this](HttpRequest* req, HttpResponse* resp) { indexfunction(req, resp); }, "GET"); | |
a9c69ea5 | 2756 | d_ws->registerWebHandler("/metrics", prometheusMetrics, "GET"); |
536ab56f | 2757 | } |
96d299db | 2758 | d_ws->go(); |
12c86877 | 2759 | } |
212f57b8 FM |
2760 | catch (...) { |
2761 | g_log << Logger::Error << "AuthWebServer thread caught an exception, dying" << endl; | |
5bd2ea7b | 2762 | _exit(1); |
12c86877 BH |
2763 | } |
2764 | } |