• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PowerDNS / pdns / 9498012098

13 Jun 2024 10:27AM UTC coverage: 61.22% (-3.4%) from 64.615%
9498012098

Pull #14325

github

web-flow
Merge e3409d021 into 57ab0fbc9
Pull Request #14325: Set nsec3param: first increase the serial and then rectify

34064 of 88172 branches covered (38.63%)

Branch coverage included in aggregate %.

5 of 7 new or added lines in 1 file covered. (71.43%)

5415 existing lines in 81 files now uncovered.

118803 of 161528 relevant lines covered (73.55%)

4993315.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.71
/pdns/ws-auth.cc
1
/*
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
 */
22
#include "dnsbackend.hh"
23
#include "webserver.hh"
24
#include <array>
25
#ifdef HAVE_CONFIG_H
26
#include "config.h"
27
#endif
28
#include "utility.hh"
29
#include "dynlistener.hh"
30
#include "ws-auth.hh"
31
#include "json.hh"
32
#include "logger.hh"
33
#include "statbag.hh"
34
#include "misc.hh"
35
#include "base64.hh"
36
#include "arguments.hh"
37
#include "dns.hh"
38
#include "comment.hh"
39
#include "ueberbackend.hh"
40
#include <boost/format.hpp>
41

42
#include "namespaces.hh"
43
#include "ws-api.hh"
44
#include "version.hh"
45
#include "dnsseckeeper.hh"
46
#include <iomanip>
47
#include "zoneparser-tng.hh"
48
#include "auth-main.hh"
49
#include "auth-caches.hh"
50
#include "auth-zonecache.hh"
51
#include "threadname.hh"
52
#include "tsigutils.hh"
53

54
using json11::Json;
55

56
Ewma::Ewma() { dt.set(); }
795✔
57

58
void Ewma::submit(int val)
59
{
750✔
60
  int rate = val - d_last;
750✔
61
  double difft = dt.udiff() / 1000000.0;
750✔
62
  dt.set();
750✔
63

64
  d_10 = ((600.0 - difft) * d_10 + (difft * rate)) / 600.0;
750✔
65
  d_5 = ((300.0 - difft) * d_5 + (difft * rate)) / 300.0;
750✔
66
  d_1 = ((60.0 - difft) * d_1 + (difft * rate)) / 60.0;
750✔
67
  d_max = max(d_1, d_max);
750✔
68

69
  d_last = val;
750✔
70
}
750✔
71

72
double Ewma::get10() const
73
{
24✔
74
  return d_10;
24✔
75
}
24✔
76

77
double Ewma::get5() const
78
{
8✔
79
  return d_5;
8✔
80
}
8✔
81

82
double Ewma::get1() const
83
{
8✔
84
  return d_1;
8✔
85
}
8✔
86

87
double Ewma::getMax() const
88
{
8✔
89
  return d_max;
8✔
90
}
8✔
91

92
static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp);
93

94
// QTypes that MUST NOT have multiple records of the same type in a given RRset.
95
static const std::set<uint16_t> onlyOneEntryTypes = {QType::CNAME, QType::DNAME, QType::SOA};
96
// QTypes that MUST NOT be used with any other QType on the same name.
97
static const std::set<uint16_t> exclusiveEntryTypes = {QType::CNAME};
98
// QTypes that MUST be at apex.
99
static const std::set<uint16_t> atApexTypes = {QType::SOA, QType::DNSKEY};
100
// QTypes that are NOT allowed at apex.
101
static const std::set<uint16_t> nonApexTypes = {QType::DS};
102

103
AuthWebServer::AuthWebServer() :
104
  d_start(time(nullptr)),
105
  d_min10(0),
106
  d_min5(0),
107
  d_min1(0)
108
{
159✔
109
  if (arg().mustDo("webserver") || arg().mustDo("api")) {
159✔
110
    d_ws = std::make_unique<WebServer>(arg()["webserver-address"], arg().asNum("webserver-port"));
12✔
111
    d_ws->setApiKey(arg()["api-key"], arg().mustDo("webserver-hash-plaintext-credentials"));
12✔
112
    d_ws->setPassword(arg()["webserver-password"], arg().mustDo("webserver-hash-plaintext-credentials"));
12✔
113
    d_ws->setLogLevel(arg()["webserver-loglevel"]);
12✔
114

115
    NetmaskGroup acl;
12✔
116
    acl.toMasks(::arg()["webserver-allow-from"]);
12✔
117
    d_ws->setACL(acl);
12✔
118

119
    d_ws->setMaxBodySize(::arg().asNum("webserver-max-bodysize"));
12✔
120
    d_ws->setConnectionTimeout(::arg().asNum("webserver-connection-timeout"));
12✔
121

122
    d_ws->bind();
12✔
123
  }
12✔
124
}
159✔
125

126
void AuthWebServer::go(StatBag& stats)
127
{
12✔
128
  S.doRings();
12✔
129
  std::thread webT([this]() { webThread(); });
12✔
130
  webT.detach();
12✔
131
  std::thread statT([this, &stats]() { statThread(stats); });
12✔
132
  statT.detach();
12✔
133
}
12✔
134

135
void AuthWebServer::statThread(StatBag& stats)
136
{
12✔
137
  try {
12✔
138
    setThreadName("pdns/statHelper");
12✔
139
    for (;;) {
150✔
140
      d_queries.submit(static_cast<int>(stats.read("udp-queries")));
150✔
141
      d_cachehits.submit(static_cast<int>(stats.read("packetcache-hit")));
150✔
142
      d_cachemisses.submit(static_cast<int>(stats.read("packetcache-miss")));
150✔
143
      d_qcachehits.submit(static_cast<int>(stats.read("query-cache-hit")));
150✔
144
      d_qcachemisses.submit(static_cast<int>(stats.read("query-cache-miss")));
150✔
145
      Utility::sleep(1);
150✔
146
    }
150✔
147
  }
12✔
148
  catch (...) {
12✔
149
    g_log << Logger::Error << "Webserver statThread caught an exception, dying" << endl;
×
150
    _exit(1);
×
151
  }
×
152
}
12✔
153

154
static string htmlescape(const string& inputString)
155
{
308✔
156
  string result;
308✔
157
  for (char currentChar : inputString) {
6,184✔
158
    switch (currentChar) {
6,184✔
159
    case '&':
×
160
      result += "&amp;";
×
161
      break;
×
162
    case '<':
×
163
      result += "&lt;";
×
164
      break;
×
165
    case '>':
×
166
      result += "&gt;";
×
167
      break;
×
168
    case '"':
32✔
169
      result += "&quot;";
32✔
170
      break;
32✔
171
    default:
6,152✔
172
      result += currentChar;
6,152✔
173
    }
6,184✔
174
  }
6,184✔
175
  return result;
308✔
176
}
308✔
177

178
static void printtable(ostringstream& ret, const string& ringname, const string& title, int limit = 10)
179
{
36✔
180
  unsigned int tot = 0;
36✔
181
  int entries = 0;
36✔
182
  vector<pair<string, unsigned int>> ring = S.getRing(ringname);
36✔
183

184
  for (const auto& entry : ring) {
52✔
185
    tot += entry.second;
52✔
186
    entries++;
52✔
187
  }
52✔
188

189
  ret << "<div class=\"panel\">";
36✔
190
  ret << "<span class=resetring><i></i><a href=\"?resetring=" << htmlescape(ringname) << "\">Reset</a></span>" << endl;
36✔
191
  ret << "<h2>" << title << "</h2>" << endl;
36✔
192
  ret << "<div class=ringmeta>";
36✔
193
  ret << "<a class=topXofY href=\"?ring=" << htmlescape(ringname) << "\">Showing: Top " << limit << " of " << entries << "</a>" << endl;
36✔
194
  ret << "<span class=resizering>Resize: ";
36✔
195
  std::vector<uint64_t> sizes{10, 100, 500, 1000, 10000, 500000, 0};
36✔
196
  for (int i = 0; sizes[i] != 0; ++i) {
252✔
197
    if (S.getRingSize(ringname) != sizes[i]) {
216✔
198
      ret << "<a href=\"?resizering=" << htmlescape(ringname) << "&amp;size=" << sizes[i] << "\">" << sizes[i] << "</a> ";
180✔
199
    }
180✔
200
    else {
36✔
201
      ret << "(" << sizes[i] << ") ";
36✔
202
    }
36✔
203
  }
216✔
204
  ret << "</span></div>";
36✔
205

206
  ret << "<table class=\"data\">";
36✔
207
  unsigned int printed = 0;
36✔
208
  unsigned int total = std::max(1U, tot);
36✔
209
  for (auto i = ring.begin(); limit != 0 && i != ring.end(); ++i, --limit) {
88✔
210
    ret << "<tr><td>" << htmlescape(i->first) << "</td><td>" << i->second << "</td><td align=right>" << AuthWebServer::makePercentage(i->second * 100.0 / total) << "</td>" << endl;
52✔
211
    printed += i->second;
52✔
212
  }
52✔
213
  ret << "<tr><td colspan=3></td></tr>" << endl;
36✔
214
  if (printed != tot) {
36!
215
    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;
×
216
  }
×
217

218
  ret << "<tr><td><b>Total:</b></td><td><b>" << tot << "</b></td><td align=right><b>100%</b></td>";
36✔
219
  ret << "</table></div>" << endl;
36✔
220
}
36✔
221

222
static void printvars(ostringstream& ret)
223
{
4✔
224
  ret << "<div class=panel><h2>Variables</h2><table class=\"data\">" << endl;
4✔
225

226
  vector<string> entries = S.getEntries();
4✔
227
  for (const auto& entry : entries) {
396✔
228
    ret << "<tr><td>" << entry << "</td><td>" << S.read(entry) << "</td><td>" << S.getDescrip(entry) << "</td>" << endl;
396✔
229
  }
396✔
230

231
  ret << "</table></div>" << endl;
4✔
232
}
4✔
233

234
static void printargs(ostringstream& ret)
235
{
×
236
  ret << R"(<table border=1><tr><td colspan=3 bgcolor="#0000ff"><font color="#ffffff">Arguments</font></td>)" << endl;
×
237

238
  vector<string> entries = arg().list();
×
239
  for (const auto& entry : entries) {
×
240
    ret << "<tr><td>" << entry << "</td><td>" << arg()[entry] << "</td><td>" << arg().getHelp(entry) << "</td>" << endl;
×
241
  }
×
242
}
×
243

244
string AuthWebServer::makePercentage(const double& val)
245
{
52✔
246
  return (boost::format("%.01f%%") % val).str();
52✔
247
}
52✔
248

249
void AuthWebServer::indexfunction(HttpRequest* req, HttpResponse* resp)
250
{
4✔
251
  if (!req->getvars["resetring"].empty()) {
4!
252
    if (S.ringExists(req->getvars["resetring"])) {
×
253
      S.resetRing(req->getvars["resetring"]);
×
254
    }
×
255
    resp->status = 302;
×
256
    resp->headers["Location"] = req->url.path;
×
257
    return;
×
258
  }
×
259
  if (!req->getvars["resizering"].empty()) {
4!
260
    int size = std::stoi(req->getvars["size"]);
×
261
    if (S.ringExists(req->getvars["resizering"]) && size > 0 && size <= 500000) {
×
262
      S.resizeRing(req->getvars["resizering"], std::stoi(req->getvars["size"]));
×
263
    }
×
264
    resp->status = 302;
×
265
    resp->headers["Location"] = req->url.path;
×
266
    return;
×
267
  }
×
268

269
  ostringstream ret;
4✔
270

271
  ret << "<!DOCTYPE html>" << endl;
4✔
272
  ret << "<html><head>" << endl;
4✔
273
  ret << "<title>PowerDNS Authoritative Server Monitor</title>" << endl;
4✔
274
  ret << R"(<link rel="stylesheet" href="style.css"/>)" << endl;
4✔
275
  ret << "</head><body>" << endl;
4✔
276

277
  ret << "<div class=\"row\">" << endl;
4✔
278
  ret << "<div class=\"headl columns\">";
4✔
279
  ret << R"(<a href="/" id="appname">PowerDNS )" << htmlescape(VERSION);
4✔
280
  if (!arg()["config-name"].empty()) {
4!
281
    ret << " [" << htmlescape(arg()["config-name"]) << "]";
×
282
  }
×
283
  ret << "</a></div>" << endl;
4✔
284
  ret << "<div class=\"header columns\"></div></div>";
4✔
285
  ret << R"(<div class="row"><div class="all columns">)";
4✔
286

287
  time_t passed = time(nullptr) - g_starttime;
4✔
288

289
  ret << "<p>Uptime: " << humanDuration(passed) << "<br>" << endl;
4✔
290

291
  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;
4✔
292

293
  if (d_cachemisses.get10() + d_cachehits.get10() > 0) {
4!
294
    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;
×
295
  }
×
296

297
  if (d_qcachemisses.get10() + d_qcachehits.get10() > 0) {
4!
UNCOV
298
    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;
×
UNCOV
299
  }
×
300

301
  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;
4✔
302

303
  ret << "Total queries: " << S.read("udp-queries") << ". Question/answer latency: " << static_cast<double>(S.read("latency")) / 1000.0 << "ms</p><br>" << endl;
4✔
304
  if (req->getvars["ring"].empty()) {
4!
305
    auto entries = S.listRings();
4✔
306
    for (const auto& entry : entries) {
36✔
307
      printtable(ret, entry, S.getRingTitle(entry));
36✔
308
    }
36✔
309

310
    printvars(ret);
4✔
311
    if (arg().mustDo("webserver-print-arguments")) {
4!
312
      printargs(ret);
×
313
    }
×
314
  }
4✔
315
  else if (S.ringExists(req->getvars["ring"])) {
×
316
    printtable(ret, req->getvars["ring"], S.getRingTitle(req->getvars["ring"]), 100);
×
317
  }
×
318

319
  ret << "</div></div>" << endl;
4✔
320
  ret << "<footer class=\"row\">" << fullVersionString() << "<br>&copy; <a href=\"https://www.powerdns.com/\">PowerDNS.COM BV</a>.</footer>" << endl;
4✔
321
  ret << "</body></html>" << endl;
4✔
322

323
  resp->body = ret.str();
4✔
324
  resp->status = 200;
4✔
325
}
4✔
326

327
/** Helper to build a record content as needed. */
328
static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot)
329
{
83,283✔
330
  // noDot: for backend storage, pass true. for API users, pass false.
331
  auto drc = DNSRecordContent::make(qtype.getCode(), QClass::IN, content);
83,283✔
332
  return drc->getZoneRepresentation(noDot);
83,283✔
333
}
83,283✔
334

335
/** "Normalize" record content for API consumers. */
336
static inline string makeApiRecordContent(const QType& qtype, const string& content)
337
{
82,930✔
338
  return makeRecordContent(qtype, content, false);
82,930✔
339
}
82,930✔
340

341
/** "Normalize" record content for backend storage. */
342
static inline string makeBackendRecordContent(const QType& qtype, const string& content)
343
{
353✔
344
  return makeRecordContent(qtype, content, true);
353✔
345
}
353✔
346

347
static Json::object getZoneInfo(const DomainInfo& domainInfo, DNSSECKeeper* dnssecKeeper)
348
{
2,092✔
349
  string zoneId = apiZoneNameToId(domainInfo.zone);
2,092✔
350
  vector<string> primaries;
2,092✔
351
  primaries.reserve(domainInfo.primaries.size());
2,092✔
352
  for (const auto& primary : domainInfo.primaries) {
2,092✔
353
    primaries.push_back(primary.toStringWithPortExcept(53));
311✔
354
  }
311✔
355

356
  auto obj = Json::object{
2,092✔
357
    // id is the canonical lookup key, which doesn't actually match the name (in some cases)
358
    {"id", zoneId},
2,092✔
359
    {"url", "/api/v1/servers/localhost/zones/" + zoneId},
2,092✔
360
    {"name", domainInfo.zone.toString()},
2,092✔
361
    {"kind", domainInfo.getKindString()},
2,092✔
362
    {"catalog", (!domainInfo.catalog.empty() ? domainInfo.catalog.toString() : "")},
2,092✔
363
    {"account", domainInfo.account},
2,092✔
364
    {"masters", std::move(primaries)},
2,092✔
365
    {"serial", (double)domainInfo.serial},
2,092✔
366
    {"notified_serial", (double)domainInfo.notified_serial},
2,092✔
367
    {"last_check", (double)domainInfo.last_check}};
2,092✔
368
  if (dnssecKeeper != nullptr) {
2,092✔
369
    obj["dnssec"] = dnssecKeeper->isSecuredZone(domainInfo.zone);
2,080✔
370
    string soa_edit;
2,080✔
371
    dnssecKeeper->getSoaEdit(domainInfo.zone, soa_edit, false);
2,080✔
372
    obj["edited_serial"] = (double)calculateEditSOA(domainInfo.serial, soa_edit, domainInfo.zone);
2,080✔
373
  }
2,080✔
374
  return obj;
2,092✔
375
}
2,092✔
376

377
static bool shouldDoRRSets(HttpRequest* req)
378
{
652✔
379
  if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true") {
652✔
380
    return true;
640✔
381
  }
640✔
382
  if (req->getvars["rrsets"] == "false") {
12✔
383
    return false;
8✔
384
  }
8✔
385

386
  throw ApiException("'rrsets' request parameter value '" + req->getvars["rrsets"] + "' is not supported");
4✔
387
}
12✔
388

389
static void fillZone(UeberBackend& backend, const DNSName& zonename, HttpResponse* resp, HttpRequest* req)
390
{
652✔
391
  DomainInfo domainInfo;
652✔
392

393
  if (!backend.getDomainInfo(zonename, domainInfo)) {
652!
394
    throw HttpNotFoundException();
×
395
  }
×
396

397
  DNSSECKeeper dnssecKeeper(&backend);
652✔
398
  Json::object doc = getZoneInfo(domainInfo, &dnssecKeeper);
652✔
399
  // extra stuff getZoneInfo doesn't do for us (more expensive)
400
  string soa_edit_api;
652✔
401
  domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api);
652✔
402
  doc["soa_edit_api"] = soa_edit_api;
652✔
403
  string soa_edit;
652✔
404
  domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
652✔
405
  doc["soa_edit"] = soa_edit;
652✔
406

407
  string nsec3param;
652✔
408
  bool nsec3narrowbool = false;
652✔
409
  bool is_secured = dnssecKeeper.isSecuredZone(zonename);
652✔
410
  if (is_secured) { // ignore NSEC3PARAM and NSEC3NARROW metadata present in the db for unsigned zones
652✔
411
    domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
69✔
412
    string nsec3narrow;
69✔
413
    domainInfo.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
69✔
414
    if (nsec3narrow == "1") {
69✔
415
      nsec3narrowbool = true;
8✔
416
    }
8✔
417
  }
69✔
418
  doc["nsec3param"] = nsec3param;
652✔
419
  doc["nsec3narrow"] = nsec3narrowbool;
652✔
420
  doc["dnssec"] = is_secured;
652✔
421

422
  string api_rectify;
652✔
423
  domainInfo.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
652✔
424
  doc["api_rectify"] = (api_rectify == "1");
652✔
425

426
  // TSIG
427
  vector<string> tsig_primary;
652✔
428
  vector<string> tsig_secondary;
652✔
429
  domainInfo.backend->getDomainMetadata(zonename, "TSIG-ALLOW-AXFR", tsig_primary);
652✔
430
  domainInfo.backend->getDomainMetadata(zonename, "AXFR-MASTER-TSIG", tsig_secondary);
652✔
431

432
  Json::array tsig_primary_keys;
652✔
433
  for (const auto& keyname : tsig_primary) {
652!
434
    tsig_primary_keys.emplace_back(apiZoneNameToId(DNSName(keyname)));
×
435
  }
×
436
  doc["master_tsig_key_ids"] = tsig_primary_keys;
652✔
437

438
  Json::array tsig_secondary_keys;
652✔
439
  for (const auto& keyname : tsig_secondary) {
652!
440
    tsig_secondary_keys.emplace_back(apiZoneNameToId(DNSName(keyname)));
×
441
  }
×
442
  doc["slave_tsig_key_ids"] = tsig_secondary_keys;
652✔
443

444
  if (shouldDoRRSets(req)) {
652✔
445
    vector<DNSResourceRecord> records;
640✔
446
    vector<Comment> comments;
640✔
447

448
    // load all records + sort
449
    {
640✔
450
      DNSResourceRecord resourceRecord;
640✔
451
      if (req->getvars.count("rrset_name") == 0) {
640✔
452
        domainInfo.backend->list(zonename, static_cast<int>(domainInfo.id), true); // incl. disabled
628✔
453
      }
628✔
454
      else {
12✔
455
        QType qType;
12✔
456
        if (req->getvars.count("rrset_type") == 0) {
12✔
457
          qType = QType::ANY;
8✔
458
        }
8✔
459
        else {
4✔
460
          qType = req->getvars["rrset_type"];
4✔
461
        }
4✔
462
        domainInfo.backend->lookup(qType, DNSName(req->getvars["rrset_name"]), static_cast<int>(domainInfo.id));
12✔
463
      }
12✔
464
      while (domainInfo.backend->get(resourceRecord)) {
83,175✔
465
        if (resourceRecord.qtype.getCode() == 0) {
82,535✔
466
          continue; // skip empty non-terminals
7✔
467
        }
7✔
468
        records.push_back(resourceRecord);
82,528✔
469
      }
82,528✔
470
      sort(records.begin(), records.end(), [](const DNSResourceRecord& rrA, const DNSResourceRecord& rrB) {
1,456,003✔
471
        /* if you ever want to update this comparison function,
472
           please be aware that you will also need to update the conditions in the code merging
473
           the records and comments below */
474
        if (rrA.qname == rrB.qname) {
1,456,003✔
475
          return rrB.qtype < rrA.qtype;
3,050✔
476
        }
3,050✔
477
        return rrB.qname < rrA.qname;
1,452,953✔
478
      });
1,456,003✔
479
    }
640✔
480

481
    // load all comments + sort
482
    {
640✔
483
      Comment comment;
640✔
484
      domainInfo.backend->listComments(domainInfo.id);
640✔
485
      while (domainInfo.backend->getComment(comment)) {
658✔
486
        comments.push_back(comment);
18✔
487
      }
18✔
488
      sort(comments.begin(), comments.end(), [](const Comment& rrA, const Comment& rrB) {
640✔
489
        /* if you ever want to update this comparison function,
490
           please be aware that you will also need to update the conditions in the code merging
491
           the records and comments below */
492
        if (rrA.qname == rrB.qname) {
10!
493
          return rrB.qtype < rrA.qtype;
10✔
494
        }
10✔
495
        return rrB.qname < rrA.qname;
×
496
      });
10✔
497
    }
640✔
498

499
    Json::array rrsets;
640✔
500
    Json::object rrset;
640✔
501
    Json::array rrset_records;
640✔
502
    Json::array rrset_comments;
640✔
503
    DNSName current_qname;
640✔
504
    QType current_qtype;
640✔
505
    uint32_t ttl = 0;
640✔
506
    auto rit = records.begin();
640✔
507
    auto cit = comments.begin();
640✔
508

509
    while (rit != records.end() || cit != comments.end()) {
82,251!
510
      // 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
511
      if (cit == comments.end() || (rit != records.end() && (rit->qname == cit->qname ? (cit->qtype < rit->qtype || cit->qtype == rit->qtype) : cit->qname < rit->qname))) {
81,611!
512
        current_qname = rit->qname;
81,611✔
513
        current_qtype = rit->qtype;
81,611✔
514
        ttl = rit->ttl;
81,611✔
515
      }
81,611✔
516
      else {
×
517
        current_qname = cit->qname;
×
518
        current_qtype = cit->qtype;
×
519
        ttl = 0;
×
520
      }
×
521

522
      while (rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
164,139✔
523
        ttl = min(ttl, rit->ttl);
82,528✔
524
        rrset_records.push_back(Json::object{
82,528✔
525
          {"disabled", rit->disabled},
82,528✔
526
          {"content", makeApiRecordContent(rit->qtype, rit->content)}});
82,528✔
527
        rit++;
82,528✔
528
      }
82,528✔
529
      while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
81,629!
530
        rrset_comments.push_back(Json::object{
18✔
531
          {"modified_at", (double)cit->modified_at},
18✔
532
          {"account", cit->account},
18✔
533
          {"content", cit->content}});
18✔
534
        cit++;
18✔
535
      }
18✔
536

537
      rrset["name"] = current_qname.toString();
81,611✔
538
      rrset["type"] = current_qtype.toString();
81,611✔
539
      rrset["records"] = rrset_records;
81,611✔
540
      rrset["comments"] = rrset_comments;
81,611✔
541
      rrset["ttl"] = (double)ttl;
81,611✔
542
      rrsets.emplace_back(rrset);
81,611✔
543
      rrset.clear();
81,611✔
544
      rrset_records.clear();
81,611✔
545
      rrset_comments.clear();
81,611✔
546
    }
81,611✔
547

548
    doc["rrsets"] = rrsets;
640✔
549
  }
640✔
550

551
  resp->setJsonBody(doc);
652✔
552
}
652✔
553

554
void productServerStatisticsFetch(map<string, string>& out)
555
{
4✔
556
  vector<string> items = S.getEntries();
4✔
557
  for (const string& item : items) {
396✔
558
    out[item] = std::to_string(S.read(item));
396✔
559
  }
396✔
560

561
  // add uptime
562
  out["uptime"] = std::to_string(time(nullptr) - g_starttime);
4✔
563
}
4✔
564

565
std::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
566
{
8✔
567
  try {
8✔
568
    // ::read() calls ::exists() which throws a PDNSException when the key does not exist
569
    return S.read(name);
8✔
570
  }
8✔
571
  catch (...) {
8✔
572
    return std::nullopt;
4✔
573
  }
4✔
574
}
8✔
575

576
static void validateGatheredRRType(const DNSResourceRecord& resourceRecord)
577
{
389✔
578
  if (resourceRecord.qtype.getCode() == QType::OPT || resourceRecord.qtype.getCode() == QType::TSIG) {
389!
579
    throw ApiException("RRset " + resourceRecord.qname.toString() + " IN " + resourceRecord.qtype.toString() + ": invalid type given");
4✔
580
  }
4✔
581
}
389✔
582

583
static void gatherRecords(const Json& container, const DNSName& qname, const QType& qtype, const uint32_t ttl, vector<DNSResourceRecord>& new_records)
584
{
297✔
585
  DNSResourceRecord resourceRecord;
297✔
586
  resourceRecord.qname = qname;
297✔
587
  resourceRecord.qtype = qtype;
297✔
588
  resourceRecord.auth = true;
297✔
589
  resourceRecord.ttl = ttl;
297✔
590

591
  validateGatheredRRType(resourceRecord);
297✔
592
  const auto& items = container["records"].array_items();
297✔
593
  for (const auto& record : items) {
357✔
594
    string content = stringFromJson(record, "content");
357✔
595
    if (record.object_items().count("priority") > 0) {
357!
596
      throw std::runtime_error("`priority` element is not allowed in record");
×
597
    }
×
598
    resourceRecord.disabled = false;
357✔
599
    if (!record["disabled"].is_null()) {
357✔
600
      resourceRecord.disabled = boolFromJson(record, "disabled");
213✔
601
    }
213✔
602

603
    // validate that the client sent something we can actually parse, and require that data to be dotted.
604
    try {
357✔
605
      if (resourceRecord.qtype.getCode() != QType::AAAA) {
357✔
606
        string tmp = makeApiRecordContent(resourceRecord.qtype, content);
330✔
607
        if (!pdns_iequals(tmp, content)) {
330✔
608
          throw std::runtime_error("Not in expected format (parsed as '" + tmp + "')");
4✔
609
        }
4✔
610
      }
330✔
611
      else {
27✔
612
        struct in6_addr tmpbuf
27✔
613
        {
27✔
614
        };
27✔
615
        if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
27!
616
          throw std::runtime_error("Invalid IPv6 address");
×
617
        }
×
618
      }
27✔
619
      resourceRecord.content = makeBackendRecordContent(resourceRecord.qtype, content);
353✔
620
    }
353✔
621
    catch (std::exception& e) {
357✔
622
      throw ApiException("Record " + resourceRecord.qname.toString() + "/" + resourceRecord.qtype.toString() + " '" + content + "': " + e.what());
4✔
623
    }
4✔
624

625
    new_records.push_back(resourceRecord);
353✔
626
  }
353✔
627
}
297✔
628

629
static void gatherComments(const Json& container, const DNSName& qname, const QType& qtype, vector<Comment>& new_comments)
630
{
25✔
631
  Comment comment;
25✔
632
  comment.qname = qname;
25✔
633
  comment.qtype = qtype;
25✔
634

635
  time_t now = time(nullptr);
25✔
636
  for (const auto& currentComment : container["comments"].array_items()) {
25✔
637
    // FIXME 2036 issue internally in uintFromJson
638
    comment.modified_at = uintFromJson(currentComment, "modified_at", now);
25✔
639
    comment.content = stringFromJson(currentComment, "content");
25✔
640
    comment.account = stringFromJson(currentComment, "account");
25✔
641
    new_comments.push_back(comment);
25✔
642
  }
25✔
643
}
25✔
644

645
static void checkDefaultDNSSECAlgos()
646
{
86✔
647
  int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
86✔
648
  int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
86✔
649
  int k_size = arg().asNum("default-ksk-size");
86✔
650
  int z_size = arg().asNum("default-zsk-size");
86✔
651

652
  // Sanity check DNSSEC parameters
653
  if (!::arg()["default-zsk-algorithm"].empty()) {
86!
654
    if (k_algo == -1) {
×
655
      throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
×
656
    }
×
657
    if (k_algo <= 10 && k_size == 0) {
×
658
      throw ApiException("default-ksk-algorithm is set to an algorithm(" + ::arg()["default-ksk-algorithm"] + ") that requires a non-zero default-ksk-size!");
×
659
    }
×
660
  }
×
661

662
  if (!::arg()["default-zsk-algorithm"].empty()) {
86!
663
    if (z_algo == -1) {
×
664
      throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
×
665
    }
×
666
    if (z_algo <= 10 && z_size == 0) {
×
667
      throw ApiException("default-zsk-algorithm is set to an algorithm(" + ::arg()["default-zsk-algorithm"] + ") that requires a non-zero default-zsk-size!");
×
668
    }
×
669
  }
×
670
}
86✔
671

672
static void throwUnableToSecure(const DNSName& zonename)
673
{
×
674
  throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
×
675
                     + "capable backends are loaded, or because the backends have DNSSEC disabled. Check your configuration.");
×
676
}
×
677

678
/*
679
 * Add KSK and ZSK to an existing zone. Algorithms and sizes will be chosen per configuration.
680
 */
681
static void addDefaultDNSSECKeys(DNSSECKeeper& dnssecKeeper, const DNSName& zonename)
682
{
45✔
683
  checkDefaultDNSSECAlgos();
45✔
684
  int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
45✔
685
  int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
45✔
686
  int k_size = arg().asNum("default-ksk-size");
45✔
687
  int z_size = arg().asNum("default-zsk-size");
45✔
688

689
  if (k_algo != -1) {
45!
690
    int64_t keyID{-1};
45✔
691
    if (!dnssecKeeper.addKey(zonename, true, k_algo, keyID, k_size)) {
45!
692
      throwUnableToSecure(zonename);
×
693
    }
×
694
  }
45✔
695

696
  if (z_algo != -1) {
45!
697
    int64_t keyID{-1};
×
698
    if (!dnssecKeeper.addKey(zonename, false, z_algo, keyID, z_size)) {
×
699
      throwUnableToSecure(zonename);
×
700
    }
×
701
  }
×
702
}
45✔
703

704
static bool isZoneApiRectifyEnabled(const DomainInfo& domainInfo)
705
{
566✔
706
  string api_rectify;
566✔
707
  domainInfo.backend->getDomainMetadataOne(domainInfo.zone, "API-RECTIFY", api_rectify);
566✔
708
  if (api_rectify.empty() && ::arg().mustDo("default-api-rectify")) {
566!
709
    api_rectify = "1";
544✔
710
  }
544✔
711
  return api_rectify == "1";
566✔
712
}
566✔
713

714
static void extractDomainInfoFromDocument(const Json& document, boost::optional<DomainInfo::DomainKind>& kind, boost::optional<vector<ComboAddress>>& primaries, boost::optional<DNSName>& catalog, boost::optional<string>& account)
715
{
1,081✔
716
  if (document["kind"].is_string()) {
1,081✔
717
    kind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
1,025✔
718
  }
1,025✔
719
  else {
56✔
720
    kind = boost::none;
56✔
721
  }
56✔
722

723
  if (document["masters"].is_array()) {
1,081✔
724
    primaries = vector<ComboAddress>();
80✔
725
    for (const auto& value : document["masters"].array_items()) {
88✔
726
      string primary = value.string_value();
88✔
727
      if (primary.empty()) {
88!
728
        throw ApiException("Primary can not be an empty string");
×
729
      }
×
730
      try {
88✔
731
        primaries->emplace_back(primary, 53);
88✔
732
      }
88✔
733
      catch (const PDNSException& e) {
88✔
734
        throw ApiException("Primary (" + primary + ") is not an IP address: " + e.reason);
×
735
      }
×
736
    }
88✔
737
  }
80✔
738
  else {
1,001✔
739
    primaries = boost::none;
1,001✔
740
  }
1,001✔
741

742
  if (document["catalog"].is_string()) {
1,081✔
743
    string catstring = document["catalog"].string_value();
16✔
744
    catalog = (!catstring.empty() ? DNSName(catstring) : DNSName());
16✔
745
  }
16✔
746
  else {
1,065✔
747
    catalog = boost::none;
1,065✔
748
  }
1,065✔
749

750
  if (document["account"].is_string()) {
1,081✔
751
    account = document["account"].string_value();
8✔
752
  }
8✔
753
  else {
1,073✔
754
    account = boost::none;
1,073✔
755
  }
1,073✔
756
}
1,081✔
757

758
/*
759
 * Build vector of TSIG Key ids from domain update document.
760
 * jsonArray: JSON array element to extract TSIG key ids from.
761
 * metadata: returned list of domain key ids for setDomainMetadata
762
 */
763
static void extractJsonTSIGKeyIds(UeberBackend& backend, const Json& jsonArray, vector<string>& metadata)
764
{
8✔
765
  for (const auto& value : jsonArray.array_items()) {
8!
766
    auto keyname(apiZoneIdToName(value.string_value()));
8✔
767
    DNSName keyAlgo;
8✔
768
    string keyContent;
8✔
769
    if (!backend.getTSIGKey(keyname, keyAlgo, keyContent)) {
8!
770
      throw ApiException("A TSIG key with the name '" + keyname.toLogString() + "' does not exist");
8✔
771
    }
8✔
772
    metadata.push_back(keyname.toString());
×
773
  }
×
774
}
8✔
775

776
// Must be called within backend transaction.
777
static void updateDomainSettingsFromDocument(UeberBackend& backend, DomainInfo& domainInfo, const DNSName& zonename, const Json& document, bool zoneWasModified)
778
{
562✔
779
  boost::optional<DomainInfo::DomainKind> kind;
562✔
780
  boost::optional<vector<ComboAddress>> primaries;
562✔
781
  boost::optional<DNSName> catalog;
562✔
782
  boost::optional<string> account;
562✔
783

784
  extractDomainInfoFromDocument(document, kind, primaries, catalog, account);
562✔
785

786
  if (kind) {
562✔
787
    domainInfo.backend->setKind(zonename, *kind);
506✔
788
    domainInfo.kind = *kind;
506✔
789
  }
506✔
790
  if (primaries) {
562✔
791
    domainInfo.backend->setPrimaries(zonename, *primaries);
44✔
792
  }
44✔
793
  if (catalog) {
562✔
794
    domainInfo.backend->setCatalog(zonename, *catalog);
12✔
795
  }
12✔
796
  if (account) {
562✔
797
    domainInfo.backend->setAccount(zonename, *account);
4✔
798
  }
4✔
799

800
  if (document["soa_edit_api"].is_string()) {
562✔
801
    domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
105✔
802
  }
105✔
803
  if (document["soa_edit"].is_string()) {
562✔
804
    domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
52✔
805
  }
52✔
806
  try {
562✔
807
    bool api_rectify = boolFromJson(document, "api_rectify");
562✔
808
    domainInfo.backend->setDomainMetadataOne(zonename, "API-RECTIFY", api_rectify ? "1" : "0");
562✔
809
  }
562✔
810
  catch (const JsonException&) {
562✔
811
  }
548✔
812

813
  DNSSECKeeper dnssecKeeper(&backend);
562✔
814
  bool shouldRectify = zoneWasModified;
562✔
815
  bool dnssecInJSON = false;
562✔
816
  bool dnssecDocVal = false;
562✔
817
  bool nsec3paramInJSON = false;
562✔
818
  bool updateNsec3Param = false;
562✔
819
  string nsec3paramDocVal;
562✔
820

821
  try {
562✔
822
    dnssecDocVal = boolFromJson(document, "dnssec");
562✔
823
    dnssecInJSON = true;
562✔
824
  }
562✔
825
  catch (const JsonException&) {
562✔
826
  }
473✔
827

828
  try {
562✔
829
    nsec3paramDocVal = stringFromJson(document, "nsec3param");
562✔
830
    nsec3paramInJSON = true;
562✔
831
  }
562✔
832
  catch (const JsonException&) {
562✔
833
  }
525✔
834

835
  bool isDNSSECZone = dnssecKeeper.isSecuredZone(zonename);
562✔
836
  bool isPresigned = dnssecKeeper.isPresigned(zonename);
562✔
837

838
  if (dnssecInJSON) {
562✔
839
    if (dnssecDocVal) {
89✔
840
      if (!isDNSSECZone) {
45!
841
        addDefaultDNSSECKeys(dnssecKeeper, zonename);
45✔
842

843
        // Used later for NSEC3PARAM
844
        isDNSSECZone = dnssecKeeper.isSecuredZone(zonename);
45✔
845

846
        if (!isDNSSECZone) {
45!
847
          throwUnableToSecure(zonename);
×
848
        }
×
849
        shouldRectify = true;
45✔
850
        updateNsec3Param = true;
45✔
851
      }
45✔
852
    }
45✔
853
    else {
44✔
854
      // "dnssec": false in json
855
      if (isDNSSECZone) {
44✔
856
        string info;
8✔
857
        string error;
8✔
858
        if (!dnssecKeeper.unSecureZone(zonename, error)) {
8!
859
          throw ApiException("Error while un-securing zone '" + zonename.toString() + "': " + error);
×
860
        }
×
861
        isDNSSECZone = dnssecKeeper.isSecuredZone(zonename, false);
8✔
862
        if (isDNSSECZone) {
8!
863
          throw ApiException("Unable to un-secure zone '" + zonename.toString() + "'");
×
864
        }
×
865
        shouldRectify = true;
8✔
866
        updateNsec3Param = true;
8✔
867
      }
8✔
868
    }
44✔
869
  }
89✔
870

871
  if (nsec3paramInJSON || updateNsec3Param) {
562✔
872
    shouldRectify = true;
65✔
873
    if (!isDNSSECZone && !nsec3paramDocVal.empty()) {
65✔
874
      throw ApiException("NSEC3PARAM value provided for zone '" + zonename.toString() + "', but zone is not DNSSEC secured.");
4✔
875
    }
4✔
876

877
    if (nsec3paramDocVal.empty()) {
61✔
878
      // Switch to NSEC
879
      if (!dnssecKeeper.unsetNSEC3PARAM(zonename)) {
36!
880
        throw ApiException("Unable to remove NSEC3PARAMs from zone '" + zonename.toString());
×
881
      }
×
882
    }
36✔
883
    else {
25✔
884
      // Set the NSEC3PARAMs
885
      NSEC3PARAMRecordContent ns3pr(nsec3paramDocVal);
25✔
886
      string error_msg;
25✔
887
      if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) {
25!
888
        throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg);
×
889
      }
×
890
      if (!dnssecKeeper.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
25!
891
        throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' passed our basic sanity checks, but cannot be used with the current backend.");
×
892
      }
×
893
    }
25✔
894
  }
61✔
895

896
  if (shouldRectify && !isPresigned) {
558!
897
    // First increase the serial (which removes the ordername from the SOA RR) if configured. And then rectify.
898

899
    // Increase serial
900
    string soa_edit_api_kind;
494✔
901
    domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
494✔
902
    if (!soa_edit_api_kind.empty()) {
494✔
903
      SOAData soaData;
417✔
904
      if (!backend.getSOAUncached(zonename, soaData)) {
417✔
905
        return;
8✔
906
      }
8✔
907

908
      string soa_edit_kind;
409✔
909
      domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
409✔
910

911
      DNSResourceRecord resourceRecord;
409✔
912
      if (makeIncreasedSOARecord(soaData, soa_edit_api_kind, soa_edit_kind, resourceRecord)) {
409!
913
        if (!domainInfo.backend->replaceRRSet(domainInfo.id, resourceRecord.qname, resourceRecord.qtype, vector<DNSResourceRecord>(1, resourceRecord))) {
409!
914
          throw ApiException("Hosting backend does not support editing records.");
×
915
        }
×
916
      }
409✔
917
    }
409✔
918

919
    // Rectify
920
    if (isZoneApiRectifyEnabled(domainInfo)) {
486✔
921
      string info;
476✔
922
      string error_msg;
476✔
923
      if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false) && !domainInfo.isSecondaryType()) {
476!
924
        // for Secondary zones, it is possible that rectifying was not needed (example: empty zone).
NEW
925
        throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
×
NEW
926
      }
×
927
    }
476✔
928
  }
486✔
929

930
  if (!document["master_tsig_key_ids"].is_null()) {
550✔
931
    vector<string> metadata;
4✔
932
    extractJsonTSIGKeyIds(backend, document["master_tsig_key_ids"], metadata);
4✔
933
    if (!domainInfo.backend->setDomainMetadata(zonename, "TSIG-ALLOW-AXFR", metadata)) {
4!
934
      throw HttpInternalServerErrorException("Unable to set new TSIG primary keys for zone '" + zonename.toLogString() + "'");
×
935
    }
×
936
  }
4✔
937
  if (!document["slave_tsig_key_ids"].is_null()) {
550✔
938
    vector<string> metadata;
4✔
939
    extractJsonTSIGKeyIds(backend, document["slave_tsig_key_ids"], metadata);
4✔
940
    if (!domainInfo.backend->setDomainMetadata(zonename, "AXFR-MASTER-TSIG", metadata)) {
4!
941
      throw HttpInternalServerErrorException("Unable to set new TSIG secondary keys for zone '" + zonename.toLogString() + "'");
×
942
    }
×
943
  }
4✔
944
}
550✔
945

946
static bool isValidMetadataKind(const string& kind, bool readonly)
947
{
56✔
948
  static vector<string> builtinOptions{
56✔
949
    "ALLOW-AXFR-FROM",
56✔
950
    "AXFR-SOURCE",
56✔
951
    "ALLOW-DNSUPDATE-FROM",
56✔
952
    "TSIG-ALLOW-DNSUPDATE",
56✔
953
    "FORWARD-DNSUPDATE",
56✔
954
    "SOA-EDIT-DNSUPDATE",
56✔
955
    "NOTIFY-DNSUPDATE",
56✔
956
    "ALSO-NOTIFY",
56✔
957
    "AXFR-MASTER-TSIG",
56✔
958
    "GSS-ALLOW-AXFR-PRINCIPAL",
56✔
959
    "GSS-ACCEPTOR-PRINCIPAL",
56✔
960
    "IXFR",
56✔
961
    "LUA-AXFR-SCRIPT",
56✔
962
    "NSEC3NARROW",
56✔
963
    "NSEC3PARAM",
56✔
964
    "PRESIGNED",
56✔
965
    "PUBLISH-CDNSKEY",
56✔
966
    "PUBLISH-CDS",
56✔
967
    "SLAVE-RENOTIFY",
56✔
968
    "SOA-EDIT",
56✔
969
    "TSIG-ALLOW-AXFR",
56✔
970
    "TSIG-ALLOW-DNSUPDATE",
56✔
971
  };
56✔
972

973
  // the following options do not allow modifications via API
974
  static vector<string> protectedOptions{
56✔
975
    "API-RECTIFY",
56✔
976
    "AXFR-MASTER-TSIG",
56✔
977
    "NSEC3NARROW",
56✔
978
    "NSEC3PARAM",
56✔
979
    "PRESIGNED",
56✔
980
    "LUA-AXFR-SCRIPT",
56✔
981
    "TSIG-ALLOW-AXFR",
56✔
982
  };
56✔
983

984
  if (kind.find("X-") == 0) {
56✔
985
    return true;
4✔
986
  }
4✔
987

988
  bool found = false;
52✔
989

990
  for (const string& builtinOption : builtinOptions) {
532!
991
    if (kind == builtinOption) {
532✔
992
      for (const string& protectedOption : protectedOptions) {
324✔
993
        if (!readonly && builtinOption == protectedOption) {
324✔
994
          return false;
16✔
995
        }
16✔
996
      }
324✔
997
      found = true;
36✔
998
      break;
36✔
999
    }
52✔
1000
  }
532✔
1001

1002
  return found;
36✔
1003
}
52✔
1004

1005
/* Return OpenAPI document describing the supported API.
1006
 */
1007
#include "apidocfiles.h"
1008

1009
void apiDocs(HttpRequest* req, HttpResponse* resp)
1010
{
×
1011
  if (req->accept_yaml) {
×
1012
    resp->setYamlBody(g_api_swagger_yaml);
×
1013
  }
×
1014
  else if (req->accept_json) {
×
1015
    resp->setJsonBody(g_api_swagger_json);
×
1016
  }
×
1017
  else {
×
1018
    resp->setPlainBody(g_api_swagger_yaml);
×
1019
  }
×
1020
}
×
1021

1022
class ZoneData
1023
{
1024
public:
1025
  ZoneData(HttpRequest* req) :
1026
    zoneName(apiZoneIdToName((req)->parameters["id"])),
1027
    dnssecKeeper(DNSSECKeeper{&backend})
1028
  {
657✔
1029
    try {
657✔
1030
      if (!backend.getDomainInfo(zoneName, domainInfo)) {
657✔
1031
        throw HttpNotFoundException();
16✔
1032
      }
16✔
1033
    }
657✔
1034
    catch (const PDNSException& e) {
657✔
1035
      throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason);
×
1036
    }
×
1037
  }
657✔
1038

1039
  DNSName zoneName;
1040
  UeberBackend backend{};
1041
  DNSSECKeeper dnssecKeeper;
1042
  DomainInfo domainInfo{};
1043
};
1044

1045
static void apiZoneMetadataGET(HttpRequest* req, HttpResponse* resp)
1046
{
4✔
1047
  ZoneData zoneData{req};
4✔
1048

1049
  map<string, vector<string>> metas;
4✔
1050
  Json::array document;
4✔
1051

1052
  if (!zoneData.backend.getAllDomainMetadata(zoneData.zoneName, metas)) {
4!
1053
    throw HttpNotFoundException();
×
1054
  }
×
1055

1056
  for (const auto& meta : metas) {
8✔
1057
    Json::array entries;
8✔
1058
    for (const string& value : meta.second) {
8✔
1059
      entries.emplace_back(value);
8✔
1060
    }
8✔
1061

1062
    Json::object key{
8✔
1063
      {"type", "Metadata"},
8✔
1064
      {"kind", meta.first},
8✔
1065
      {"metadata", entries}};
8✔
1066
    document.emplace_back(key);
8✔
1067
  }
8✔
1068
  resp->setJsonBody(document);
4✔
1069
}
4✔
1070

1071
static void apiZoneMetadataPOST(HttpRequest* req, HttpResponse* resp)
1072
{
16✔
1073
  ZoneData zoneData{req};
16✔
1074

1075
  const auto& document = req->json();
16✔
1076
  string kind;
16✔
1077
  vector<string> entries;
16✔
1078

1079
  try {
16✔
1080
    kind = stringFromJson(document, "kind");
16✔
1081
  }
16✔
1082
  catch (const JsonException&) {
16✔
1083
    throw ApiException("kind is not specified or not a string");
×
1084
  }
×
1085

1086
  if (!isValidMetadataKind(kind, false)) {
12!
1087
    throw ApiException("Unsupported metadata kind '" + kind + "'");
×
1088
  }
×
1089

1090
  vector<string> vecMetadata;
12✔
1091

1092
  if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
12!
1093
    throw ApiException("Could not retrieve metadata entries for domain '" + zoneData.zoneName.toString() + "'");
×
1094
  }
×
1095

1096
  const auto& metadata = document["metadata"];
12✔
1097
  if (!metadata.is_array()) {
12!
1098
    throw ApiException("metadata is not specified or not an array");
×
1099
  }
×
1100

1101
  for (const auto& value : metadata.array_items()) {
12✔
1102
    if (!value.is_string()) {
12!
1103
      throw ApiException("metadata must be strings");
×
1104
    }
×
1105
    if (std::find(vecMetadata.cbegin(),
12!
1106
                  vecMetadata.cend(),
12✔
1107
                  value.string_value())
12✔
1108
        == vecMetadata.cend()) {
12✔
1109
      vecMetadata.push_back(value.string_value());
12✔
1110
    }
12✔
1111
  }
12✔
1112

1113
  if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
12!
1114
    throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'");
×
1115
  }
×
1116

1117
  DNSSECKeeper::clearMetaCache(zoneData.zoneName);
12✔
1118

1119
  Json::array respMetadata;
12✔
1120
  for (const string& value : vecMetadata) {
12✔
1121
    respMetadata.emplace_back(value);
12✔
1122
  }
12✔
1123

1124
  Json::object key{
12✔
1125
    {"type", "Metadata"},
12✔
1126
    {"kind", document["kind"]},
12✔
1127
    {"metadata", respMetadata}};
12✔
1128

1129
  resp->status = 201;
12✔
1130
  resp->setJsonBody(key);
12✔
1131
}
12✔
1132

1133
static void apiZoneMetadataKindGET(HttpRequest* req, HttpResponse* resp)
1134
{
12✔
1135
  ZoneData zoneData{req};
12✔
1136

1137
  string kind = req->parameters["kind"];
12✔
1138

1139
  vector<string> metadata;
12✔
1140
  Json::object document;
12✔
1141
  Json::array entries;
12✔
1142

1143
  if (!zoneData.backend.getDomainMetadata(zoneData.zoneName, kind, metadata)) {
12!
1144
    throw HttpNotFoundException();
×
1145
  }
×
1146
  if (!isValidMetadataKind(kind, true)) {
12!
1147
    throw ApiException("Unsupported metadata kind '" + kind + "'");
×
1148
  }
×
1149

1150
  document["type"] = "Metadata";
12✔
1151
  document["kind"] = kind;
12✔
1152

1153
  for (const string& value : metadata) {
12✔
1154
    entries.emplace_back(value);
8✔
1155
  }
8✔
1156

1157
  document["metadata"] = entries;
12✔
1158
  resp->setJsonBody(document);
12✔
1159
}
12✔
1160

1161
static void apiZoneMetadataKindPUT(HttpRequest* req, HttpResponse* resp)
1162
{
24✔
1163
  ZoneData zoneData{req};
24✔
1164

1165
  string kind = req->parameters["kind"];
24✔
1166

1167
  const auto& document = req->json();
24✔
1168

1169
  if (!isValidMetadataKind(kind, false)) {
24✔
1170
    throw ApiException("Unsupported metadata kind '" + kind + "'");
16✔
1171
  }
16✔
1172

1173
  vector<string> vecMetadata;
8✔
1174
  const auto& metadata = document["metadata"];
8✔
1175
  if (!metadata.is_array()) {
8!
1176
    throw ApiException("metadata is not specified or not an array");
×
1177
  }
×
1178
  for (const auto& value : metadata.array_items()) {
8✔
1179
    if (!value.is_string()) {
8!
1180
      throw ApiException("metadata must be strings");
×
1181
    }
×
1182
    vecMetadata.push_back(value.string_value());
8✔
1183
  }
8✔
1184

1185
  if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, vecMetadata)) {
8!
1186
    throw ApiException("Could not update metadata entries for domain '" + zoneData.zoneName.toString() + "'");
×
1187
  }
×
1188

1189
  DNSSECKeeper::clearMetaCache(zoneData.zoneName);
8✔
1190

1191
  Json::object key{
8✔
1192
    {"type", "Metadata"},
8✔
1193
    {"kind", kind},
8✔
1194
    {"metadata", metadata}};
8✔
1195

1196
  resp->setJsonBody(key);
8✔
1197
}
8✔
1198

1199
static void apiZoneMetadataKindDELETE(HttpRequest* req, HttpResponse* resp)
1200
{
8✔
1201
  ZoneData zoneData{req};
8✔
1202

1203
  const string& kind = req->parameters["kind"];
8✔
1204
  if (!isValidMetadataKind(kind, false)) {
8!
1205
    throw ApiException("Unsupported metadata kind '" + kind + "'");
×
1206
  }
×
1207

1208
  vector<string> metadata; // an empty vector will do it
8✔
1209
  if (!zoneData.backend.setDomainMetadata(zoneData.zoneName, kind, metadata)) {
8!
1210
    throw ApiException("Could not delete metadata for domain '" + zoneData.zoneName.toString() + "' (" + kind + ")");
×
1211
  }
×
1212

1213
  DNSSECKeeper::clearMetaCache(zoneData.zoneName);
8✔
1214
  resp->status = 204;
8✔
1215
}
8✔
1216

1217
// Throws 404 if the key with inquireKeyId does not exist
1218
static void apiZoneCryptoKeysCheckKeyExists(const DNSName& zonename, int inquireKeyId, DNSSECKeeper* dnssecKeeper)
1219
{
44✔
1220
  DNSSECKeeper::keyset_t keyset = dnssecKeeper->getKeys(zonename, false);
44✔
1221
  bool found = false;
44✔
1222
  for (const auto& value : keyset) {
44✔
1223
    if (value.second.id == (unsigned)inquireKeyId) {
40✔
1224
      found = true;
36✔
1225
      break;
36✔
1226
    }
36✔
1227
  }
40✔
1228
  if (!found) {
44✔
1229
    throw HttpNotFoundException();
8✔
1230
  }
8✔
1231
}
44✔
1232

1233
static inline int getInquireKeyId(HttpRequest* req, const DNSName& zonename, DNSSECKeeper* dnsseckeeper)
1234
{
60✔
1235
  int inquireKeyId = -1;
60✔
1236
  if (req->parameters.count("key_id") == 1) {
60✔
1237
    inquireKeyId = std::stoi(req->parameters["key_id"]);
44✔
1238
    apiZoneCryptoKeysCheckKeyExists(zonename, inquireKeyId, dnsseckeeper);
44✔
1239
  }
44✔
1240
  return inquireKeyId;
60✔
1241
}
60✔
1242

1243
static void apiZoneCryptokeysExport(const DNSName& zonename, int64_t inquireKeyId, HttpResponse* resp, DNSSECKeeper* dnssec_dk)
1244
{
36✔
1245
  DNSSECKeeper::keyset_t keyset = dnssec_dk->getKeys(zonename, false);
36✔
1246

1247
  bool inquireSingleKey = inquireKeyId >= 0;
36✔
1248

1249
  Json::array doc;
36✔
1250
  for (const auto& value : keyset) {
36✔
1251
    if (inquireSingleKey && (unsigned)inquireKeyId != value.second.id) {
32!
1252
      continue;
×
1253
    }
×
1254

1255
    string keyType;
32✔
1256
    switch (value.second.keyType) {
32!
1257
    case DNSSECKeeper::KSK:
×
1258
      keyType = "ksk";
×
1259
      break;
×
1260
    case DNSSECKeeper::ZSK:
×
1261
      keyType = "zsk";
×
1262
      break;
×
1263
    case DNSSECKeeper::CSK:
32!
1264
      keyType = "csk";
32✔
1265
      break;
32✔
1266
    }
32✔
1267

1268
    Json::object key{
32✔
1269
      {"type", "Cryptokey"},
32✔
1270
      {"id", static_cast<int>(value.second.id)},
32✔
1271
      {"active", value.second.active},
32✔
1272
      {"published", value.second.published},
32✔
1273
      {"keytype", keyType},
32✔
1274
      {"flags", static_cast<uint16_t>(value.first.getFlags())},
32✔
1275
      {"dnskey", value.first.getDNSKEY().getZoneRepresentation()},
32✔
1276
      {"algorithm", DNSSECKeeper::algorithm2name(value.first.getAlgorithm())},
32✔
1277
      {"bits", value.first.getKey()->getBits()}};
32✔
1278

1279
    string publishCDS;
32✔
1280
    dnssec_dk->getPublishCDS(zonename, publishCDS);
32✔
1281

1282
    vector<string> digestAlgos;
32✔
1283
    stringtok(digestAlgos, publishCDS, ", ");
32✔
1284

1285
    std::set<unsigned int> CDSalgos;
32✔
1286
    for (auto const& digestAlgo : digestAlgos) {
32✔
1287
      CDSalgos.insert(pdns::checked_stoi<unsigned int>(digestAlgo));
4✔
1288
    }
4✔
1289

1290
    if (value.second.keyType == DNSSECKeeper::KSK || value.second.keyType == DNSSECKeeper::CSK) {
32!
1291
      Json::array cdses;
32✔
1292
      Json::array dses;
32✔
1293
      for (const uint8_t keyid : {DNSSECKeeper::DIGEST_SHA256, DNSSECKeeper::DIGEST_SHA384}) {
64✔
1294
        try {
64✔
1295
          string dsRecordContent = makeDSFromDNSKey(zonename, value.first.getDNSKEY(), keyid).getZoneRepresentation();
64✔
1296

1297
          dses.emplace_back(dsRecordContent);
64✔
1298

1299
          if (CDSalgos.count(keyid) != 0) {
64✔
1300
            cdses.emplace_back(dsRecordContent);
4✔
1301
          }
4✔
1302
        }
64✔
1303
        catch (...) {
64✔
1304
        }
×
1305
      }
64✔
1306

1307
      key["ds"] = dses;
32✔
1308

1309
      if (!cdses.empty()) {
32✔
1310
        key["cds"] = cdses;
4✔
1311
      }
4✔
1312
    }
32✔
1313

1314
    if (inquireSingleKey) {
32✔
1315
      key["privatekey"] = value.first.getKey()->convertToISC();
20✔
1316
      resp->setJsonBody(key);
20✔
1317
      return;
20✔
1318
    }
20✔
1319
    doc.emplace_back(key);
12✔
1320
  }
12✔
1321

1322
  if (inquireSingleKey) {
16!
1323
    // we came here because we couldn't find the requested key.
1324
    throw HttpNotFoundException();
×
1325
  }
×
1326
  resp->setJsonBody(doc);
16✔
1327
}
16✔
1328

1329
static void apiZoneCryptokeysGET(HttpRequest* req, HttpResponse* resp)
1330
{
20✔
1331
  ZoneData zoneData{req};
20✔
1332
  const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
20✔
1333

1334
  apiZoneCryptokeysExport(zoneData.zoneName, inquireKeyId, resp, &zoneData.dnssecKeeper);
20✔
1335
}
20✔
1336

1337
/*
1338
 * This method handles DELETE requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1339
 * It deletes a key from :zone_name specified by :cryptokey_id.
1340
 * Server Answers:
1341
 * Case 1: the backend returns true on removal. This means the key is gone.
1342
 *      The server returns 204 No Content, no body.
1343
 * Case 2: the backend returns false on removal. An error occurred.
1344
 *      The server returns 422 Unprocessable Entity with message "Could not DELETE :cryptokey_id".
1345
 * Case 3: the key or zone does not exist.
1346
 *      The server returns 404 Not Found
1347
 * */
1348
static void apiZoneCryptokeysDELETE(HttpRequest* req, HttpResponse* resp)
1349
{
16✔
1350
  ZoneData zoneData{req};
16✔
1351
  const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
16✔
1352

1353
  if (inquireKeyId == -1) {
16!
1354
    throw HttpBadRequestException();
×
1355
  }
×
1356

1357
  if (zoneData.dnssecKeeper.removeKey(zoneData.zoneName, inquireKeyId)) {
16✔
1358
    resp->body = "";
4✔
1359
    resp->status = 204;
4✔
1360
  }
4✔
1361
  else {
12✔
1362
    resp->setErrorResult("Could not DELETE " + req->parameters["key_id"], 422);
12✔
1363
  }
12✔
1364
}
16✔
1365

1366
/*
1367
 * This method adds a key to a zone by generate it or content parameter.
1368
 * Parameter:
1369
 *  {
1370
 *  "privatekey" : "key The format used is compatible with BIND and NSD/LDNS" <string>
1371
 *  "keytype" : "ksk|zsk" <string>
1372
 *  "active"  : "true|false" <value>
1373
 *  "algorithm" : "key generation algorithm name as default"<string> https://doc.powerdns.com/md/authoritative/dnssec/#supported-algorithms
1374
 *  "bits" : number of bits <int>
1375
 *  }
1376
 *
1377
 * Response:
1378
 *  Case 1: keytype isn't ksk|zsk
1379
 *    The server returns 422 Unprocessable Entity {"error" : "Invalid keytype 'keytype'"}
1380
 *  Case 2: 'bits' must be a positive integer value.
1381
 *    The server returns 422 Unprocessable Entity {"error" : "'bits' must be a positive integer value."}
1382
 *  Case 3: The "algorithm" isn't supported
1383
 *    The server returns 422 Unprocessable Entity {"error" : "Unknown algorithm: 'algo'"}
1384
 *  Case 4: Algorithm <= 10 and no bits were passed
1385
 *    The server returns 422 Unprocessable Entity {"error" : "Creating an algorithm algo key requires the size (in bits) to be passed"}
1386
 *  Case 5: The wrong keysize was passed
1387
 *    The server returns 422 Unprocessable Entity {"error" : "The algorithm does not support the given bit size."}
1388
 *  Case 6: If the server cant guess the keysize
1389
 *    The server returns 422 Unprocessable Entity {"error" : "Can not guess key size for algorithm"}
1390
 *  Case 7: The key-creation failed
1391
 *    The server returns 422 Unprocessable Entity {"error" : "Adding key failed, perhaps DNSSEC not enabled in configuration?"}
1392
 *  Case 8: The key in content has the wrong format
1393
 *    The server returns 422 Unprocessable Entity {"error" : "Key could not be parsed. Make sure your key format is correct."}
1394
 *  Case 9: The wrong combination of fields is submitted
1395
 *    The server returns 422 Unprocessable Entity {"error" : "Either you submit just the 'content' field or you leave 'content' empty and submit the other fields."}
1396
 *  Case 10: No content and everything was fine
1397
 *    The server returns 201 Created and all public data about the new cryptokey
1398
 *  Case 11: With specified content
1399
 *    The server returns 201 Created and all public data about the added cryptokey
1400
 */
1401

1402
static void apiZoneCryptokeysPOST(HttpRequest* req, HttpResponse* resp)
1403
{
56✔
1404
  ZoneData zoneData{req};
56✔
1405

1406
  const auto& document = req->json();
56✔
1407
  string privatekey_fieldname = "privatekey";
56✔
1408
  auto privatekey = document["privatekey"];
56✔
1409
  if (privatekey.is_null()) {
56!
1410
    // Fallback to the old "content" behaviour
1411
    privatekey = document["content"];
56✔
1412
    privatekey_fieldname = "content";
56✔
1413
  }
56✔
1414
  bool active = boolFromJson(document, "active", false);
56✔
1415
  bool published = boolFromJson(document, "published", true);
56✔
1416
  bool keyOrZone = false;
56✔
1417

1418
  if (stringFromJson(document, "keytype") == "ksk" || stringFromJson(document, "keytype") == "csk") {
56!
1419
    keyOrZone = true;
52✔
1420
  }
52✔
1421
  else if (stringFromJson(document, "keytype") == "zsk") {
4!
1422
    keyOrZone = false;
×
1423
  }
×
1424
  else {
4✔
1425
    throw ApiException("Invalid keytype " + stringFromJson(document, "keytype"));
4✔
1426
  }
4✔
1427

1428
  int64_t insertedId = -1;
52✔
1429

1430
  if (privatekey.is_null()) {
52✔
1431
    int bits = keyOrZone ? ::arg().asNum("default-ksk-size") : ::arg().asNum("default-zsk-size");
44!
1432
    auto docbits = document["bits"];
44✔
1433
    if (!docbits.is_null()) {
44✔
1434
      if (!docbits.is_number() || (fmod(docbits.number_value(), 1.0) != 0) || docbits.int_value() < 0) {
24!
1435
        throw ApiException("'bits' must be a positive integer value");
12✔
1436
      }
12✔
1437

1438
      bits = docbits.int_value();
12✔
1439
    }
12✔
1440
    int algorithm = DNSSECKeeper::shorthand2algorithm(keyOrZone ? ::arg()["default-ksk-algorithm"] : ::arg()["default-zsk-algorithm"]);
32!
1441
    const auto& providedAlgo = document["algorithm"];
32✔
1442
    if (providedAlgo.is_string()) {
32✔
1443
      algorithm = DNSSECKeeper::shorthand2algorithm(providedAlgo.string_value());
16✔
1444
      if (algorithm == -1) {
16✔
1445
        throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
4✔
1446
      }
4✔
1447
    }
16✔
1448
    else if (providedAlgo.is_number()) {
16✔
1449
      algorithm = providedAlgo.int_value();
12✔
1450
    }
12✔
1451
    else if (!providedAlgo.is_null()) {
4!
1452
      throw ApiException("Unknown algorithm: " + providedAlgo.string_value());
×
1453
    }
×
1454

1455
    try {
28✔
1456
      if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, keyOrZone, algorithm, insertedId, bits, active, published)) {
28!
1457
        throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
×
1458
      }
×
1459
    }
28✔
1460
    catch (std::runtime_error& error) {
28✔
1461
      throw ApiException(error.what());
12✔
1462
    }
12✔
1463
    if (insertedId < 0) {
16!
1464
      throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
×
1465
    }
×
1466
  }
16✔
1467
  else if (document["bits"].is_null() && document["algorithm"].is_null()) {
8!
1468
    const auto& keyData = stringFromJson(document, privatekey_fieldname);
8✔
1469
    DNSKEYRecordContent dkrc;
8✔
1470
    DNSSECPrivateKey dpk;
8✔
1471
    try {
8✔
1472
      shared_ptr<DNSCryptoKeyEngine> dke(DNSCryptoKeyEngine::makeFromISCString(dkrc, keyData));
8✔
1473
      uint16_t flags = 0;
8✔
1474
      if (keyOrZone) {
8✔
1475
        flags = 257;
4✔
1476
      }
4✔
1477
      else {
4✔
1478
        flags = 256;
4✔
1479
      }
4✔
1480

1481
      uint8_t algorithm = dkrc.d_algorithm;
8✔
1482
      // TODO remove in 4.2.0
1483
      if (algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1) {
8!
1484
        algorithm = DNSSECKeeper::RSASHA1;
×
1485
      }
×
1486
      dpk.setKey(dke, flags, algorithm);
8✔
1487
    }
8✔
1488
    catch (std::runtime_error& error) {
8✔
1489
      throw ApiException("Key could not be parsed. Make sure your key format is correct.");
4✔
1490
    }
4✔
1491
    try {
4✔
1492
      if (!zoneData.dnssecKeeper.addKey(zoneData.zoneName, dpk, insertedId, active, published)) {
4!
1493
        throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
×
1494
      }
×
1495
    }
4✔
1496
    catch (std::runtime_error& error) {
4✔
1497
      throw ApiException(error.what());
×
1498
    }
×
1499
    if (insertedId < 0) {
4!
1500
      throw ApiException("Adding key failed, perhaps DNSSEC not enabled in configuration?");
×
1501
    }
×
1502
  }
4✔
1503
  else {
×
1504
    throw ApiException("Either you submit just the 'privatekey' field or you leave 'privatekey' empty and submit the other fields.");
×
1505
  }
×
1506
  apiZoneCryptokeysExport(zoneData.zoneName, insertedId, resp, &zoneData.dnssecKeeper);
20✔
1507
  resp->status = 201;
20✔
1508
}
20✔
1509

1510
/*
1511
 * This method handles PUT (execute) requests for URL /api/v1/servers/:server_id/zones/:zone_name/cryptokeys/:cryptokey_id .
1512
 * It de/activates a key from :zone_name specified by :cryptokey_id.
1513
 * Server Answers:
1514
 * Case 1: invalid JSON data
1515
 *      The server returns 400 Bad Request
1516
 * Case 2: the backend returns true on de/activation. This means the key is de/active.
1517
 *      The server returns 204 No Content
1518
 * Case 3: the backend returns false on de/activation. An error occurred.
1519
 *      The sever returns 422 Unprocessable Entity with message "Could not de/activate Key: :cryptokey_id in Zone: :zone_name"
1520
 * */
1521
static void apiZoneCryptokeysPUT(HttpRequest* req, HttpResponse* resp)
1522
{
32✔
1523
  ZoneData zoneData{req};
32✔
1524
  const auto inquireKeyId = getInquireKeyId(req, zoneData.zoneName, &zoneData.dnssecKeeper);
32✔
1525

1526
  if (inquireKeyId == -1) {
32!
1527
    throw HttpBadRequestException();
×
1528
  }
×
1529
  // throws an exception if the Body is empty
1530
  const auto& document = req->json();
32✔
1531
  // throws an exception if the key does not exist or is not a bool
1532
  bool active = boolFromJson(document, "active");
32✔
1533
  bool published = boolFromJson(document, "published", true);
32✔
1534
  if (active) {
32✔
1535
    if (!zoneData.dnssecKeeper.activateKey(zoneData.zoneName, inquireKeyId)) {
24!
1536
      resp->setErrorResult("Could not activate Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
×
1537
      return;
×
1538
    }
×
1539
  }
24✔
1540
  else {
8✔
1541
    if (!zoneData.dnssecKeeper.deactivateKey(zoneData.zoneName, inquireKeyId)) {
8!
1542
      resp->setErrorResult("Could not deactivate Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
×
1543
      return;
×
1544
    }
×
1545
  }
8✔
1546

1547
  if (published) {
32✔
1548
    if (!zoneData.dnssecKeeper.publishKey(zoneData.zoneName, inquireKeyId)) {
24!
1549
      resp->setErrorResult("Could not publish Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
×
1550
      return;
×
1551
    }
×
1552
  }
24✔
1553
  else {
8✔
1554
    if (!zoneData.dnssecKeeper.unpublishKey(zoneData.zoneName, inquireKeyId)) {
8!
1555
      resp->setErrorResult("Could not unpublish Key: " + req->parameters["key_id"] + " in Zone: " + zoneData.zoneName.toString(), 422);
×
1556
      return;
×
1557
    }
×
1558
  }
8✔
1559

1560
  resp->body = "";
32✔
1561
  resp->status = 204;
32✔
1562
}
32✔
1563

1564
static void gatherRecordsFromZone(const std::string& zonestring, vector<DNSResourceRecord>& new_records, const DNSName& zonename)
1565
{
20✔
1566
  DNSResourceRecord resourceRecord;
20✔
1567
  vector<string> zonedata;
20✔
1568
  stringtok(zonedata, zonestring, "\r\n");
20✔
1569

1570
  ZoneParserTNG zpt(zonedata, zonename);
20✔
1571
  zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
20✔
1572
  zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
20✔
1573

1574
  bool seenSOA = false;
20✔
1575

1576
  string comment = "Imported via the API";
20✔
1577

1578
  try {
20✔
1579
    while (zpt.get(resourceRecord, &comment)) {
120✔
1580
      if (seenSOA && resourceRecord.qtype.getCode() == QType::SOA) {
100✔
1581
        continue;
8✔
1582
      }
8✔
1583
      if (resourceRecord.qtype.getCode() == QType::SOA) {
92✔
1584
        seenSOA = true;
20✔
1585
      }
20✔
1586
      validateGatheredRRType(resourceRecord);
92✔
1587

1588
      new_records.push_back(resourceRecord);
92✔
1589
    }
92✔
1590
  }
20✔
1591
  catch (std::exception& ae) {
20✔
1592
    throw ApiException("An error occurred while parsing the zonedata: " + string(ae.what()));
×
1593
  }
×
1594
}
20✔
1595

1596
/** Throws ApiException if records which violate RRset constraints are present.
1597
 *  NOTE: sorts records in-place.
1598
 *
1599
 *  Constraints being checked:
1600
 *   *) no exact duplicates
1601
 *   *) no duplicates for QTypes that can only be present once per RRset
1602
 *   *) hostnames are hostnames
1603
 */
1604
static void checkNewRecords(vector<DNSResourceRecord>& records, const DNSName& zone)
1605
{
642✔
1606
  sort(records.begin(), records.end(),
642✔
1607
       [](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool {
2,270✔
1608
         /* we need _strict_ weak ordering */
1609
         return std::tie(rec_a.qname, rec_a.qtype, rec_a.content) < std::tie(rec_b.qname, rec_b.qtype, rec_b.content);
2,270✔
1610
       });
2,270✔
1611

1612
  DNSResourceRecord previous;
642✔
1613
  for (const auto& rec : records) {
1,682✔
1614
    if (previous.qname == rec.qname) {
1,682✔
1615
      if (previous.qtype == rec.qtype) {
1,013✔
1616
        if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
499✔
1617
          throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " has more than one record");
8✔
1618
        }
8✔
1619
        if (previous.content == rec.content) {
491✔
1620
          throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + " with content \"" + rec.content + "\"");
4✔
1621
        }
4✔
1622
      }
491✔
1623
      else if (exclusiveEntryTypes.count(rec.qtype.getCode()) != 0 || exclusiveEntryTypes.count(previous.qtype.getCode()) != 0) {
514!
1624
        throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + ": Conflicts with another RRset");
8✔
1625
      }
8✔
1626
    }
1,013✔
1627

1628
    if (rec.qname == zone) {
1,662✔
1629
      if (nonApexTypes.count(rec.qtype.getCode()) != 0) {
1,543✔
1630
        throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is not allowed at apex");
4✔
1631
      }
4✔
1632
    }
1,543✔
1633
    else if (atApexTypes.count(rec.qtype.getCode()) != 0) {
119✔
1634
      throw ApiException("Record " + rec.qname.toString() + " IN " + rec.qtype.toString() + " is only allowed at apex");
8✔
1635
    }
8✔
1636

1637
    // Check if the DNSNames that should be hostnames, are hostnames
1638
    try {
1,650✔
1639
      checkHostnameCorrectness(rec);
1,650✔
1640
    }
1,650✔
1641
    catch (const std::exception& e) {
1,650✔
1642
      throw ApiException("RRset " + rec.qname.toString() + " IN " + rec.qtype.toString() + ": " + e.what());
8✔
1643
    }
8✔
1644

1645
    previous = rec;
1,642✔
1646
  }
1,642✔
1647
}
642✔
1648

1649
static void checkTSIGKey(UeberBackend& backend, const DNSName& keyname, const DNSName& algo, const string& content)
1650
{
48✔
1651
  DNSName algoFromDB;
48✔
1652
  string contentFromDB;
48✔
1653
  if (backend.getTSIGKey(keyname, algoFromDB, contentFromDB)) {
48✔
1654
    throw HttpConflictException("A TSIG key with the name '" + keyname.toLogString() + "' already exists");
4✔
1655
  }
4✔
1656

1657
  TSIGHashEnum the{};
44✔
1658
  if (!getTSIGHashEnum(algo, the)) {
44!
1659
    throw ApiException("Unknown TSIG algorithm: " + algo.toLogString());
×
1660
  }
×
1661

1662
  string b64out;
44✔
1663
  if (B64Decode(content, b64out) == -1) {
44✔
1664
    throw ApiException("TSIG content '" + content + "' cannot be base64-decoded");
4✔
1665
  }
4✔
1666
}
44✔
1667

1668
static Json::object makeJSONTSIGKey(const DNSName& keyname, const DNSName& algo, const string& content)
1669
{
92✔
1670
  Json::object tsigkey = {
92✔
1671
    {"name", keyname.toStringNoDot()},
92✔
1672
    {"id", apiZoneNameToId(keyname)},
92✔
1673
    {"algorithm", algo.toStringNoDot()},
92✔
1674
    {"key", content},
92✔
1675
    {"type", "TSIGKey"}};
92✔
1676
  return tsigkey;
92✔
1677
}
92✔
1678

1679
static Json::object makeJSONTSIGKey(const struct TSIGKey& key, bool doContent = true)
1680
{
52✔
1681
  return makeJSONTSIGKey(key.name, key.algorithm, doContent ? key.key : "");
52✔
1682
}
52✔
1683

1684
static void apiServerTSIGKeysGET(HttpRequest* /* req */, HttpResponse* resp)
1685
{
4✔
1686
  UeberBackend backend;
4✔
1687
  vector<struct TSIGKey> keys;
4✔
1688

1689
  if (!backend.getTSIGKeys(keys)) {
4!
1690
    throw HttpInternalServerErrorException("Unable to retrieve TSIG keys");
×
1691
  }
×
1692

1693
  Json::array doc;
4✔
1694

1695
  for (const auto& key : keys) {
40✔
1696
    doc.emplace_back(makeJSONTSIGKey(key, false));
40✔
1697
  }
40✔
1698
  resp->setJsonBody(doc);
4✔
1699
}
4✔
1700

1701
static void apiServerTSIGKeysPOST(HttpRequest* req, HttpResponse* resp)
1702
{
52✔
1703
  UeberBackend backend;
52✔
1704
  const auto& document = req->json();
52✔
1705
  DNSName keyname(stringFromJson(document, "name"));
52✔
1706
  DNSName algo(stringFromJson(document, "algorithm"));
52✔
1707
  string content = document["key"].string_value();
52✔
1708

1709
  if (content.empty()) {
52✔
1710
    try {
44✔
1711
      content = makeTSIGKey(algo);
44✔
1712
    }
44✔
1713
    catch (const PDNSException& exc) {
44✔
1714
      throw HttpBadRequestException(exc.reason);
4✔
1715
    }
4✔
1716
  }
44✔
1717

1718
  // Will throw an ApiException or HttpConflictException on error
1719
  checkTSIGKey(backend, keyname, algo, content);
48✔
1720

1721
  if (!backend.setTSIGKey(keyname, algo, content)) {
48!
1722
    throw HttpInternalServerErrorException("Unable to add TSIG key");
×
1723
  }
×
1724

1725
  resp->status = 201;
48✔
1726
  resp->setJsonBody(makeJSONTSIGKey(keyname, algo, content));
48✔
1727
}
48✔
1728

1729
class TSIGKeyData
1730
{
1731
public:
1732
  TSIGKeyData(HttpRequest* req) :
1733
    keyName(apiZoneIdToName(req->parameters["id"]))
1734
  {
36✔
1735
    try {
36✔
1736
      if (!backend.getTSIGKey(keyName, algo, content)) {
36✔
1737
        throw HttpNotFoundException("TSIG key with name '" + keyName.toLogString() + "' not found");
12✔
1738
      }
12✔
1739
    }
36✔
1740
    catch (const PDNSException& e) {
36✔
1741
      throw HttpInternalServerErrorException("Could not retrieve Domain Info: " + e.reason);
×
1742
    }
×
1743

1744
    tsigKey.name = keyName;
24✔
1745
    tsigKey.algorithm = algo;
24✔
1746
    tsigKey.key = std::move(content);
24✔
1747
  }
24✔
1748

1749
  UeberBackend backend;
1750
  DNSName keyName;
1751
  DNSName algo;
1752
  string content;
1753
  struct TSIGKey tsigKey;
1754
};
1755

1756
static void apiServerTSIGKeyDetailGET(HttpRequest* req, HttpResponse* resp)
1757
{
8✔
1758
  TSIGKeyData tsigKeyData{req};
8✔
1759

1760
  resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey));
8✔
1761
}
8✔
1762

1763
static void apiServerTSIGKeyDetailPUT(HttpRequest* req, HttpResponse* resp)
1764
{
24✔
1765
  TSIGKeyData tsigKeyData{req};
24✔
1766

1767
  const auto& document = req->json();
24✔
1768

1769
  if (document["name"].is_string()) {
24✔
1770
    tsigKeyData.tsigKey.name = DNSName(document["name"].string_value());
4✔
1771
  }
4✔
1772
  if (document["algorithm"].is_string()) {
24✔
1773
    tsigKeyData.tsigKey.algorithm = DNSName(document["algorithm"].string_value());
8✔
1774

1775
    TSIGHashEnum the{};
8✔
1776
    if (!getTSIGHashEnum(tsigKeyData.tsigKey.algorithm, the)) {
8✔
1777
      throw ApiException("Unknown TSIG algorithm: " + tsigKeyData.tsigKey.algorithm.toLogString());
4✔
1778
    }
4✔
1779
  }
8✔
1780
  if (document["key"].is_string()) {
20✔
1781
    string new_content = document["key"].string_value();
8✔
1782
    string decoded;
8✔
1783
    if (B64Decode(new_content, decoded) == -1) {
8✔
1784
      throw ApiException("Can not base64 decode key content '" + new_content + "'");
4✔
1785
    }
4✔
1786
    tsigKeyData.tsigKey.key = std::move(new_content);
4✔
1787
  }
4✔
1788
  if (!tsigKeyData.backend.setTSIGKey(tsigKeyData.tsigKey.name, tsigKeyData.tsigKey.algorithm, tsigKeyData.tsigKey.key)) {
16!
1789
    throw HttpInternalServerErrorException("Unable to save TSIG Key");
×
1790
  }
×
1791
  if (tsigKeyData.tsigKey.name != tsigKeyData.keyName) {
16✔
1792
    // Remove the old key
1793
    if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) {
4!
1794
      throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'");
×
1795
    }
×
1796
  }
4✔
1797
  resp->setJsonBody(makeJSONTSIGKey(tsigKeyData.tsigKey));
16✔
1798
}
16✔
1799

1800
static void apiServerTSIGKeyDetailDELETE(HttpRequest* req, HttpResponse* resp)
1801
{
4✔
1802
  TSIGKeyData tsigKeyData{req};
4✔
1803
  if (!tsigKeyData.backend.deleteTSIGKey(tsigKeyData.keyName)) {
4!
1804
    throw HttpInternalServerErrorException("Unable to remove TSIG key '" + tsigKeyData.keyName.toStringNoDot() + "'");
×
1805
  }
×
1806
  resp->body = "";
4✔
1807
  resp->status = 204;
4✔
1808
}
4✔
1809

1810
static void apiServerAutoprimaryDetailDELETE(HttpRequest* req, HttpResponse* resp)
1811
{
3✔
1812
  UeberBackend backend;
3✔
1813
  const AutoPrimary& primary{req->parameters["ip"], req->parameters["nameserver"], ""};
3✔
1814
  if (!backend.autoPrimaryRemove(primary)) {
3!
1815
    throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
×
1816
  }
×
1817
  resp->body = "";
3✔
1818
  resp->status = 204;
3✔
1819
}
3✔
1820

1821
static void apiServerAutoprimariesGET(HttpRequest* /* req */, HttpResponse* resp)
1822
{
12✔
1823
  UeberBackend backend;
12✔
1824

1825
  std::vector<AutoPrimary> primaries;
12✔
1826
  if (!backend.autoPrimariesList(primaries)) {
12!
1827
    throw HttpInternalServerErrorException("Unable to retrieve autoprimaries");
×
1828
  }
×
1829
  Json::array doc;
12✔
1830
  for (const auto& primary : primaries) {
12✔
1831
    const Json::object obj = {
12✔
1832
      {"ip", primary.ip},
12✔
1833
      {"nameserver", primary.nameserver},
12✔
1834
      {"account", primary.account}};
12✔
1835
    doc.emplace_back(obj);
12✔
1836
  }
12✔
1837
  resp->setJsonBody(doc);
12✔
1838
}
12✔
1839

1840
static void apiServerAutoprimariesPOST(HttpRequest* req, HttpResponse* resp)
1841
{
6✔
1842
  UeberBackend backend;
6✔
1843

1844
  const auto& document = req->json();
6✔
1845

1846
  AutoPrimary primary(stringFromJson(document, "ip"), stringFromJson(document, "nameserver"), "");
6✔
1847

1848
  if (document["account"].is_string()) {
6✔
1849
    primary.account = document["account"].string_value();
3✔
1850
  }
3✔
1851

1852
  if (primary.ip.empty() or primary.nameserver.empty()) {
6!
1853
    throw ApiException("ip and nameserver fields must be filled");
×
1854
  }
×
1855
  if (!backend.autoPrimaryAdd(primary)) {
6!
1856
    throw HttpInternalServerErrorException("Cannot find backend with autoprimary feature");
×
1857
  }
×
1858
  resp->body = "";
6✔
1859
  resp->status = 201;
6✔
1860
}
6✔
1861

1862
// create new zone
1863
static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp)
1864
{
539✔
1865
  UeberBackend backend;
539✔
1866
  DNSSECKeeper dnssecKeeper(&backend);
539✔
1867
  DomainInfo domainInfo;
539✔
1868
  const auto& document = req->json();
539✔
1869
  DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
539✔
1870
  apiCheckNameAllowedCharacters(zonename.toString());
539✔
1871
  zonename.makeUsLowerCase();
539✔
1872

1873
  bool exists = backend.getDomainInfo(zonename, domainInfo);
539✔
1874
  if (exists) {
539✔
1875
    throw HttpConflictException();
4✔
1876
  }
4✔
1877

1878
  boost::optional<DomainInfo::DomainKind> kind;
535✔
1879
  boost::optional<vector<ComboAddress>> primaries;
535✔
1880
  boost::optional<DNSName> catalog;
535✔
1881
  boost::optional<string> account;
535✔
1882
  extractDomainInfoFromDocument(document, kind, primaries, catalog, account);
535✔
1883

1884
  // validate 'kind' is set
1885
  if (!kind) {
535!
1886
    throw JsonException("Key 'kind' not present or not a String");
×
1887
  }
×
1888
  DomainInfo::DomainKind zonekind = *kind;
535✔
1889

1890
  string zonestring = document["zone"].string_value();
535✔
1891
  auto rrsets = document["rrsets"];
535✔
1892
  if (rrsets.is_array() && !zonestring.empty()) {
535!
1893
    throw ApiException("You cannot give rrsets AND zone data as text");
×
1894
  }
×
1895

1896
  const auto& nameservers = document["nameservers"];
535✔
1897
  if (!nameservers.is_null() && !nameservers.is_array() && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) {
535!
1898
    throw ApiException("Nameservers is not a list");
×
1899
  }
×
1900

1901
  // if records/comments are given, load and check them
1902
  bool have_soa = false;
535✔
1903
  bool have_zone_ns = false;
535✔
1904
  vector<DNSResourceRecord> new_records;
535✔
1905
  vector<Comment> new_comments;
535✔
1906

1907
  try {
535✔
1908
    if (rrsets.is_array()) {
535✔
1909
      for (const auto& rrset : rrsets.array_items()) {
66✔
1910
        DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
66✔
1911
        apiCheckQNameAllowedCharacters(qname.toString());
66✔
1912
        QType qtype;
66✔
1913
        qtype = stringFromJson(rrset, "type");
66✔
1914
        if (qtype.getCode() == 0) {
66!
1915
          throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
×
1916
        }
×
1917
        if (rrset["records"].is_array()) {
66✔
1918
          uint32_t ttl = uintFromJson(rrset, "ttl");
62✔
1919
          gatherRecords(rrset, qname, qtype, ttl, new_records);
62✔
1920
        }
62✔
1921
        if (rrset["comments"].is_array()) {
66✔
1922
          gatherComments(rrset, qname, qtype, new_comments);
11✔
1923
        }
11✔
1924
      }
66✔
1925
    }
44✔
1926
    else if (!zonestring.empty()) {
491✔
1927
      gatherRecordsFromZone(zonestring, new_records, zonename);
20✔
1928
    }
20✔
1929
  }
535✔
1930
  catch (const JsonException& exc) {
535✔
1931
    throw ApiException("New RRsets are invalid: " + string(exc.what()));
×
1932
  }
×
1933

1934
  if (zonekind == DomainInfo::Consumer && !new_records.empty()) {
519✔
1935
    throw ApiException("Zone data MUST NOT be given for Consumer zones");
8✔
1936
  }
8✔
1937

1938
  for (auto& resourceRecord : new_records) {
511✔
1939
    resourceRecord.qname.makeUsLowerCase();
146✔
1940
    if (!resourceRecord.qname.isPartOf(zonename) && resourceRecord.qname != zonename) {
146!
1941
      throw ApiException("RRset " + resourceRecord.qname.toString() + " IN " + resourceRecord.qtype.toString() + ": Name is out of zone");
4✔
1942
    }
4✔
1943

1944
    apiCheckQNameAllowedCharacters(resourceRecord.qname.toString());
142✔
1945

1946
    if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) {
142!
1947
      have_soa = true;
20✔
1948
    }
20✔
1949
    if (resourceRecord.qtype.getCode() == QType::NS && resourceRecord.qname == zonename) {
142✔
1950
      have_zone_ns = true;
32✔
1951
    }
32✔
1952
  }
142✔
1953

1954
  // synthesize RRs as needed
1955
  DNSResourceRecord autorr;
507✔
1956
  autorr.qname = zonename;
507✔
1957
  autorr.auth = true;
507✔
1958
  autorr.ttl = ::arg().asNum("default-ttl");
507✔
1959

1960
  if (!have_soa && zonekind != DomainInfo::Secondary && zonekind != DomainInfo::Consumer) {
507✔
1961
    // synthesize a SOA record so the zone "really" exists
1962
    string soa = ::arg()["default-soa-content"];
451✔
1963
    boost::replace_all(soa, "@", zonename.toStringNoDot());
451✔
1964
    SOAData soaData;
451✔
1965
    fillSOAData(soa, soaData);
451✔
1966
    soaData.serial = document["serial"].int_value();
451✔
1967
    autorr.qtype = QType::SOA;
451✔
1968
    autorr.content = makeSOAContent(soaData)->getZoneRepresentation(true);
451✔
1969
    // updateDomainSettingsFromDocument will apply SOA-EDIT-API as needed
1970
    new_records.push_back(autorr);
451✔
1971
  }
451✔
1972

1973
  // create NS records if nameservers are given
1974
  for (const auto& value : nameservers.array_items()) {
898✔
1975
    const string& nameserver = value.string_value();
898✔
1976
    if (nameserver.empty()) {
898✔
1977
      throw ApiException("Nameservers must be non-empty strings");
4✔
1978
    }
4✔
1979
    if (zonekind == DomainInfo::Consumer) {
894✔
1980
      throw ApiException("Nameservers MUST NOT be given for Consumer zones");
4✔
1981
    }
4✔
1982
    if (!isCanonical(nameserver)) {
890✔
1983
      throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
4✔
1984
    }
4✔
1985
    try {
886✔
1986
      // ensure the name parses
1987
      autorr.content = DNSName(nameserver).toStringRootDot();
886✔
1988
    }
886✔
1989
    catch (...) {
886✔
1990
      throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
×
1991
    }
×
1992
    autorr.qtype = QType::NS;
886✔
1993
    new_records.push_back(autorr);
886✔
1994
    if (have_zone_ns) {
886✔
1995
      throw ApiException("Nameservers list MUST NOT be mixed with zone-level NS in rrsets");
4✔
1996
    }
4✔
1997
  }
886✔
1998

1999
  checkNewRecords(new_records, zonename);
491✔
2000

2001
  if (boolFromJson(document, "dnssec", false)) {
491✔
2002
    checkDefaultDNSSECAlgos();
41✔
2003

2004
    if (document["nsec3param"].string_value().length() > 0) {
41✔
2005
      NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
25✔
2006
      string error_msg;
25✔
2007
      if (!dnssecKeeper.checkNSEC3PARAM(ns3pr, error_msg)) {
25!
2008
        throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() + "' are invalid. " + error_msg);
×
2009
      }
×
2010
    }
25✔
2011
  }
41✔
2012

2013
  // no going back after this
2014
  if (!backend.createDomain(zonename, kind.get_value_or(DomainInfo::Native), primaries.get_value_or(vector<ComboAddress>()), account.get_value_or(""))) {
491!
2015
    throw ApiException("Creating domain '" + zonename.toString() + "' failed: backend refused");
×
2016
  }
×
2017

2018
  if (!backend.getDomainInfo(zonename, domainInfo)) {
491!
2019
    throw ApiException("Creating domain '" + zonename.toString() + "' failed: lookup of domain ID failed");
×
2020
  }
×
2021

2022
  domainInfo.backend->startTransaction(zonename, static_cast<int>(domainInfo.id));
491✔
2023

2024
  // will be overridden by updateDomainSettingsFromDocument, if given in document.
2025
  domainInfo.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", "DEFAULT");
491✔
2026

2027
  for (auto& resourceRecord : new_records) {
1,435✔
2028
    resourceRecord.domain_id = static_cast<int>(domainInfo.id);
1,435✔
2029
    domainInfo.backend->feedRecord(resourceRecord, DNSName());
1,435✔
2030
  }
1,435✔
2031
  for (Comment& comment : new_comments) {
491✔
2032
    comment.domain_id = static_cast<int>(domainInfo.id);
10✔
2033
    if (!domainInfo.backend->feedComment(comment)) {
10✔
2034
      throw ApiException("Hosting backend does not support editing comments.");
1✔
2035
    }
1✔
2036
  }
10✔
2037

2038
  updateDomainSettingsFromDocument(backend, domainInfo, zonename, document, !new_records.empty());
490✔
2039

2040
  if (!catalog && kind == DomainInfo::Primary) {
490✔
2041
    const auto& defaultCatalog = ::arg()["default-catalog-zone"];
16✔
2042
    if (!defaultCatalog.empty()) {
16!
2043
      domainInfo.backend->setCatalog(zonename, DNSName(defaultCatalog));
16✔
2044
    }
16✔
2045
  }
16✔
2046

2047
  domainInfo.backend->commitTransaction();
490✔
2048

2049
  g_zoneCache.add(zonename, static_cast<int>(domainInfo.id)); // make new zone visible
490✔
2050

2051
  fillZone(backend, zonename, resp, req);
490✔
2052
  resp->status = 201;
490✔
2053
}
490✔
2054

2055
// list known zones
2056
static void apiServerZonesGET(HttpRequest* req, HttpResponse* resp)
2057
{
55✔
2058
  UeberBackend backend;
55✔
2059
  DNSSECKeeper dnssecKeeper(&backend);
55✔
2060
  vector<DomainInfo> domains;
55✔
2061

2062
  if (req->getvars.count("zone") != 0) {
55✔
2063
    string zone = req->getvars["zone"];
4✔
2064
    apiCheckNameAllowedCharacters(zone);
4✔
2065
    DNSName zonename = apiNameToDNSName(zone);
4✔
2066
    zonename.makeUsLowerCase();
4✔
2067
    DomainInfo domainInfo;
4✔
2068
    if (backend.getDomainInfo(zonename, domainInfo)) {
4!
2069
      domains.push_back(domainInfo);
4✔
2070
    }
4✔
2071
  }
4✔
2072
  else {
51✔
2073
    try {
51✔
2074
      backend.getAllDomains(&domains, true, true); // incl. serial and disabled
51✔
2075
    }
51✔
2076
    catch (const PDNSException& exception) {
51✔
2077
      throw HttpInternalServerErrorException("Could not retrieve all domain information: " + exception.reason);
×
2078
    }
×
2079
  }
51✔
2080

2081
  bool with_dnssec = true;
55✔
2082
  if (req->getvars.count("dnssec") != 0) {
55✔
2083
    // can send ?dnssec=false to improve performance.
2084
    string dnssec_flag = req->getvars["dnssec"];
4✔
2085
    if (dnssec_flag == "false") {
4!
2086
      with_dnssec = false;
4✔
2087
    }
4✔
2088
  }
4✔
2089

2090
  Json::array doc;
55✔
2091
  doc.reserve(domains.size());
55✔
2092
  for (const DomainInfo& domainInfo : domains) {
1,440✔
2093
    doc.emplace_back(getZoneInfo(domainInfo, with_dnssec ? &dnssecKeeper : nullptr));
1,440✔
2094
  }
1,440✔
2095
  resp->setJsonBody(doc);
55✔
2096
}
55✔
2097

2098
static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp)
2099
{
108✔
2100
  ZoneData zoneData{req};
108✔
2101

2102
  // update domain contents and/or settings
2103
  const auto& document = req->json();
108✔
2104

2105
  auto rrsets = document["rrsets"];
108✔
2106
  bool zoneWasModified = false;
108✔
2107
  DomainInfo::DomainKind newKind = zoneData.domainInfo.kind;
108✔
2108
  if (document["kind"].is_string()) {
108✔
2109
    newKind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
20✔
2110
  }
20✔
2111

2112
  // if records/comments are given, load, check and insert them
2113
  if (rrsets.is_array()) {
108✔
2114
    zoneWasModified = true;
56✔
2115
    bool haveSoa = false;
56✔
2116
    string soaEditApiKind;
56✔
2117
    string soaEditKind;
56✔
2118
    zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT-API", soaEditApiKind);
56✔
2119
    zoneData.domainInfo.backend->getDomainMetadataOne(zoneData.zoneName, "SOA-EDIT", soaEditKind);
56✔
2120

2121
    vector<DNSResourceRecord> new_records;
56✔
2122
    vector<Comment> new_comments;
56✔
2123

2124
    try {
56✔
2125
      for (const auto& rrset : rrsets.array_items()) {
120✔
2126
        DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
120✔
2127
        apiCheckQNameAllowedCharacters(qname.toString());
120✔
2128
        QType qtype;
120✔
2129
        qtype = stringFromJson(rrset, "type");
120✔
2130
        if (qtype.getCode() == 0) {
120✔
2131
          throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
4✔
2132
        }
4✔
2133
        if (rrset["records"].is_array()) {
116✔
2134
          uint32_t ttl = uintFromJson(rrset, "ttl");
112✔
2135
          gatherRecords(rrset, qname, qtype, ttl, new_records);
112✔
2136
        }
112✔
2137
        if (rrset["comments"].is_array()) {
116!
2138
          gatherComments(rrset, qname, qtype, new_comments);
×
2139
        }
×
2140
      }
116✔
2141
    }
56✔
2142
    catch (const JsonException& exc) {
56✔
2143
      throw ApiException("New RRsets are invalid: " + string(exc.what()));
4✔
2144
    }
4✔
2145

2146
    for (auto& resourceRecord : new_records) {
120✔
2147
      resourceRecord.qname.makeUsLowerCase();
120✔
2148
      if (!resourceRecord.qname.isPartOf(zoneData.zoneName) && resourceRecord.qname != zoneData.zoneName) {
120!
2149
        throw ApiException("RRset " + resourceRecord.qname.toString() + " IN " + resourceRecord.qtype.toString() + ": Name is out of zone");
4✔
2150
      }
4✔
2151
      apiCheckQNameAllowedCharacters(resourceRecord.qname.toString());
116✔
2152

2153
      if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zoneData.zoneName) {
116!
2154
        haveSoa = true;
32✔
2155
      }
32✔
2156
    }
116✔
2157

2158
    if (!haveSoa && newKind != DomainInfo::Secondary && newKind != DomainInfo::Consumer) {
40✔
2159
      // Require SOA if this is a primary zone.
2160
      throw ApiException("Must give SOA record for zone when replacing all RR sets");
4✔
2161
    }
4✔
2162
    if (newKind == DomainInfo::Consumer && !new_records.empty()) {
36✔
2163
      // Allow deleting all RRsets, just not modifying them.
2164
      throw ApiException("Modifying RRsets in Consumer zones is unsupported");
4✔
2165
    }
4✔
2166

2167
    checkNewRecords(new_records, zoneData.zoneName);
32✔
2168

2169
    zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id));
32✔
2170
    for (auto& resourceRecord : new_records) {
60✔
2171
      resourceRecord.domain_id = static_cast<int>(zoneData.domainInfo.id);
60✔
2172
      zoneData.domainInfo.backend->feedRecord(resourceRecord, DNSName());
60✔
2173
    }
60✔
2174
    for (Comment& comment : new_comments) {
32!
2175
      comment.domain_id = static_cast<int>(zoneData.domainInfo.id);
×
2176
      zoneData.domainInfo.backend->feedComment(comment);
×
2177
    }
×
2178

2179
    if (!haveSoa && (newKind == DomainInfo::Secondary || newKind == DomainInfo::Consumer)) {
32!
2180
      zoneData.domainInfo.backend->setStale(zoneData.domainInfo.id);
8✔
2181
    }
8✔
2182
  }
32✔
2183
  else {
52✔
2184
    // avoid deleting current zone contents
2185
    zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1);
52✔
2186
  }
52✔
2187

2188
  // updateDomainSettingsFromDocument will rectify the zone and update SOA serial.
2189
  updateDomainSettingsFromDocument(zoneData.backend, zoneData.domainInfo, zoneData.zoneName, document, zoneWasModified);
84✔
2190
  zoneData.domainInfo.backend->commitTransaction();
84✔
2191

2192
  purgeAuthCaches(zoneData.zoneName.toString() + "$");
84✔
2193

2194
  resp->body = "";
84✔
2195
  resp->status = 204; // No Content, but indicate success
84✔
2196
}
84✔
2197

2198
static void apiServerZoneDetailDELETE(HttpRequest* req, HttpResponse* resp)
2199
{
20✔
2200
  ZoneData zoneData{req};
20✔
2201

2202
  // delete domain
2203

2204
  zoneData.domainInfo.backend->startTransaction(zoneData.zoneName, -1);
20✔
2205
  try {
20✔
2206
    if (!zoneData.domainInfo.backend->deleteDomain(zoneData.zoneName)) {
20!
2207
      throw ApiException("Deleting domain '" + zoneData.zoneName.toString() + "' failed: backend delete failed/unsupported");
×
2208
    }
×
2209

2210
    zoneData.domainInfo.backend->commitTransaction();
20✔
2211

2212
    g_zoneCache.remove(zoneData.zoneName);
20✔
2213
  }
20✔
2214
  catch (...) {
20✔
2215
    zoneData.domainInfo.backend->abortTransaction();
×
2216
    throw;
×
2217
  }
×
2218

2219
  // clear caches
2220
  DNSSECKeeper::clearCaches(zoneData.zoneName);
16✔
2221
  purgeAuthCaches(zoneData.zoneName.toString() + "$");
16✔
2222

2223
  // empty body on success
2224
  resp->body = "";
16✔
2225
  resp->status = 204; // No Content: declare that the zone is gone now
16✔
2226
}
16✔
2227

2228
static void apiServerZoneDetailPATCH(HttpRequest* req, HttpResponse* resp)
2229
{
152✔
2230
  ZoneData zoneData{req};
152✔
2231
  patchZone(zoneData.backend, zoneData.zoneName, zoneData.domainInfo, req, resp);
152✔
2232
}
152✔
2233

2234
static void apiServerZoneDetailGET(HttpRequest* req, HttpResponse* resp)
2235
{
166✔
2236
  ZoneData zoneData{req};
166✔
2237
  fillZone(zoneData.backend, zoneData.zoneName, resp, req);
166✔
2238
}
166✔
2239

2240
static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp)
2241
{
8✔
2242
  ZoneData zoneData{req};
8✔
2243

2244
  ostringstream outputStringStream;
8✔
2245

2246
  DNSResourceRecord resourceRecord;
8✔
2247
  SOAData soaData;
8✔
2248
  zoneData.domainInfo.backend->list(zoneData.zoneName, static_cast<int>(zoneData.domainInfo.id));
8✔
2249
  while (zoneData.domainInfo.backend->get(resourceRecord)) {
32✔
2250
    if (resourceRecord.qtype.getCode() == 0) {
24!
2251
      continue; // skip empty non-terminals
×
2252
    }
×
2253

2254
    outputStringStream << resourceRecord.qname.toString() << "\t" << resourceRecord.ttl << "\t"
24✔
2255
                       << "IN"
24✔
2256
                       << "\t" << resourceRecord.qtype.toString() << "\t" << makeApiRecordContent(resourceRecord.qtype, resourceRecord.content) << endl;
24✔
2257
  }
24✔
2258

2259
  if (req->accept_json) {
8✔
2260
    resp->setJsonBody(Json::object{{"zone", outputStringStream.str()}});
4✔
2261
  }
4✔
2262
  else {
4✔
2263
    resp->headers["Content-Type"] = "text/plain; charset=us-ascii";
4✔
2264
    resp->body = outputStringStream.str();
4✔
2265
  }
4✔
2266
}
8✔
2267

2268
static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp)
2269
{
4✔
2270
  ZoneData zoneData{req};
4✔
2271

2272
  if (zoneData.domainInfo.primaries.empty()) {
4!
2273
    throw ApiException("Domain '" + zoneData.zoneName.toString() + "' is not a secondary domain (or has no primary defined)");
×
2274
  }
×
2275

2276
  shuffle(zoneData.domainInfo.primaries.begin(), zoneData.domainInfo.primaries.end(), pdns::dns_random_engine());
4✔
2277
  Communicator.addSuckRequest(zoneData.zoneName, zoneData.domainInfo.primaries.front(), SuckRequest::Api);
4✔
2278
  resp->setSuccessResult("Added retrieval request for '" + zoneData.zoneName.toString() + "' from primary " + zoneData.domainInfo.primaries.front().toLogString());
4✔
2279
}
4✔
2280

2281
static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp)
2282
{
4✔
2283
  ZoneData zoneData{req};
4✔
2284

2285
  if (!Communicator.notifyDomain(zoneData.zoneName, &zoneData.backend)) {
4!
2286
    throw ApiException("Failed to add to the queue - see server log");
×
2287
  }
×
2288

2289
  resp->setSuccessResult("Notification queued");
4✔
2290
}
4✔
2291

2292
static void apiServerZoneRectify(HttpRequest* req, HttpResponse* resp)
2293
{
7✔
2294
  ZoneData zoneData{req};
7✔
2295

2296
  if (zoneData.dnssecKeeper.isPresigned(zoneData.zoneName)) {
7!
2297
    throw ApiException("Zone '" + zoneData.zoneName.toString() + "' is pre-signed, not rectifying.");
×
2298
  }
×
2299

2300
  string error_msg;
7✔
2301
  string info;
7✔
2302
  if (!zoneData.dnssecKeeper.rectifyZone(zoneData.zoneName, error_msg, info, true)) {
7!
2303
    throw ApiException("Failed to rectify '" + zoneData.zoneName.toString() + "' " + error_msg);
×
2304
  }
×
2305

2306
  resp->setSuccessResult("Rectified");
7✔
2307
}
7✔
2308

2309
// NOLINTNEXTLINE(readability-function-cognitive-complexity): TODO Refactor this function.
2310
static void patchZone(UeberBackend& backend, const DNSName& zonename, DomainInfo& domainInfo, HttpRequest* req, HttpResponse* resp)
2311
{
152✔
2312
  bool zone_disabled = false;
152✔
2313
  SOAData soaData;
152✔
2314

2315
  vector<DNSResourceRecord> new_records;
152✔
2316
  vector<Comment> new_comments;
152✔
2317
  vector<DNSResourceRecord> new_ptrs;
152✔
2318

2319
  Json document = req->json();
152✔
2320

2321
  auto rrsets = document["rrsets"];
152✔
2322
  if (!rrsets.is_array()) {
152!
2323
    throw ApiException("No rrsets given in update request");
×
2324
  }
×
2325

2326
  domainInfo.backend->startTransaction(zonename);
152✔
2327

2328
  try {
152✔
2329
    string soa_edit_api_kind;
152✔
2330
    string soa_edit_kind;
152✔
2331
    domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT-API", soa_edit_api_kind);
152✔
2332
    domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit_kind);
152✔
2333
    bool soa_edit_done = false;
152✔
2334

2335
    set<std::tuple<DNSName, QType, string>> seen;
152✔
2336

2337
    for (const auto& rrset : rrsets.array_items()) {
168✔
2338
      string changetype = toUpper(stringFromJson(rrset, "changetype"));
168✔
2339
      DNSName qname = apiNameToDNSName(stringFromJson(rrset, "name"));
168✔
2340
      apiCheckQNameAllowedCharacters(qname.toString());
168✔
2341
      QType qtype;
168✔
2342
      qtype = stringFromJson(rrset, "type");
168✔
2343
      if (qtype.getCode() == 0) {
168✔
2344
        throw ApiException("RRset " + qname.toString() + " IN " + stringFromJson(rrset, "type") + ": unknown type given");
4✔
2345
      }
4✔
2346

2347
      if (seen.count({qname, qtype, changetype}) != 0) {
164✔
2348
        throw ApiException("Duplicate RRset " + qname.toString() + " IN " + qtype.toString() + " with changetype: " + changetype);
4✔
2349
      }
4✔
2350
      seen.insert({qname, qtype, changetype});
160✔
2351

2352
      if (changetype == "DELETE") {
160✔
2353
        // delete all matching qname/qtype RRs (and, implicitly comments).
2354
        if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, vector<DNSResourceRecord>())) {
11!
2355
          throw ApiException("Hosting backend does not support editing records.");
×
2356
        }
×
2357
      }
11✔
2358
      else if (changetype == "REPLACE") {
149✔
2359
        // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
2360
        if (!qname.isPartOf(zonename) && qname != zonename) {
145!
2361
          throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Name is out of zone");
4✔
2362
        }
4✔
2363

2364
        bool replace_records = rrset["records"].is_array();
141✔
2365
        bool replace_comments = rrset["comments"].is_array();
141✔
2366

2367
        if (!replace_records && !replace_comments) {
141!
2368
          throw ApiException("No change for RRset " + qname.toString() + " IN " + qtype.toString());
×
2369
        }
×
2370

2371
        new_records.clear();
141✔
2372
        new_comments.clear();
141✔
2373

2374
        try {
141✔
2375
          if (replace_records) {
141✔
2376
            // ttl shouldn't be part of DELETE, and it shouldn't be required if we don't get new records.
2377
            uint32_t ttl = uintFromJson(rrset, "ttl");
127✔
2378
            gatherRecords(rrset, qname, qtype, ttl, new_records);
127✔
2379

2380
            for (DNSResourceRecord& resourceRecord : new_records) {
147✔
2381
              resourceRecord.domain_id = static_cast<int>(domainInfo.id);
147✔
2382
              if (resourceRecord.qtype.getCode() == QType::SOA && resourceRecord.qname == zonename) {
147✔
2383
                soa_edit_done = increaseSOARecord(resourceRecord, soa_edit_api_kind, soa_edit_kind);
24✔
2384
              }
24✔
2385
            }
147✔
2386
            checkNewRecords(new_records, zonename);
127✔
2387
          }
127✔
2388

2389
          if (replace_comments) {
141✔
2390
            gatherComments(rrset, qname, qtype, new_comments);
14✔
2391

2392
            for (Comment& comment : new_comments) {
14✔
2393
              comment.domain_id = static_cast<int>(domainInfo.id);
11✔
2394
            }
11✔
2395
          }
14✔
2396
        }
141✔
2397
        catch (const JsonException& e) {
141✔
2398
          throw ApiException("New RRsets are invalid: " + string(e.what()));
3✔
2399
        }
3✔
2400

2401
        if (replace_records) {
102✔
2402
          bool ent_present = false;
91✔
2403
          bool dname_seen = false;
91✔
2404
          bool ns_seen = false;
91✔
2405

2406
          domainInfo.backend->lookup(QType(QType::ANY), qname, static_cast<int>(domainInfo.id));
91✔
2407
          DNSResourceRecord resourceRecord;
91✔
2408
          while (domainInfo.backend->get(resourceRecord)) {
244✔
2409
            if (resourceRecord.qtype.getCode() == QType::ENT) {
161✔
2410
              ent_present = true;
4✔
2411
              /* that's fine, we will override it */
2412
              continue;
4✔
2413
            }
4✔
2414
            if (qtype == QType::DNAME || resourceRecord.qtype == QType::DNAME) {
157!
2415
              dname_seen = true;
16✔
2416
            }
16✔
2417
            if (qtype == QType::NS || resourceRecord.qtype == QType::NS) {
157✔
2418
              ns_seen = true;
121✔
2419
            }
121✔
2420
            if (qtype.getCode() != resourceRecord.qtype.getCode()
157✔
2421
                && (exclusiveEntryTypes.count(qtype.getCode()) != 0
157✔
2422
                    || exclusiveEntryTypes.count(resourceRecord.qtype.getCode()) != 0)) {
115✔
2423

2424
              // leave database handle in a consistent state
2425
              while (domainInfo.backend->get(resourceRecord)) {
16✔
2426
                ;
8✔
2427
              }
8✔
2428

2429
              throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Conflicts with pre-existing RRset");
8✔
2430
            }
8✔
2431
          }
157✔
2432

2433
          if (dname_seen && ns_seen && qname != zonename) {
83!
2434
            throw ApiException("RRset " + qname.toString() + " IN " + qtype.toString() + ": Cannot have both NS and DNAME except in zone apex");
4✔
2435
          }
4✔
2436
          if (!new_records.empty() && domainInfo.kind == DomainInfo::Consumer) {
79!
2437
            // Allow deleting all RRsets, just not modifying them.
2438
            throw ApiException("Modifying RRsets in Consumer zones is unsupported");
×
2439
          }
×
2440
          if (!new_records.empty() && ent_present) {
79!
2441
            QType qt_ent{0};
4✔
2442
            if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qt_ent, new_records)) {
4!
2443
              throw ApiException("Hosting backend does not support editing records.");
×
2444
            }
×
2445
          }
4✔
2446
          if (!domainInfo.backend->replaceRRSet(domainInfo.id, qname, qtype, new_records)) {
79!
2447
            throw ApiException("Hosting backend does not support editing records.");
×
2448
          }
×
2449
        }
79✔
2450
        if (replace_comments) {
90✔
2451
          if (!domainInfo.backend->replaceComments(domainInfo.id, qname, qtype, new_comments)) {
11✔
2452
            throw ApiException("Hosting backend does not support editing comments.");
1✔
2453
          }
1✔
2454
        }
11✔
2455
      }
90✔
2456
      else {
4✔
2457
        throw ApiException("Changetype not understood");
4✔
2458
      }
4✔
2459
    }
160✔
2460

2461
    zone_disabled = (!backend.getSOAUncached(zonename, soaData));
84✔
2462

2463
    // edit SOA (if needed)
2464
    if (!zone_disabled && !soa_edit_api_kind.empty() && !soa_edit_done) {
84✔
2465
      DNSResourceRecord resourceRecord;
64✔
2466
      if (makeIncreasedSOARecord(soaData, soa_edit_api_kind, soa_edit_kind, resourceRecord)) {
64!
2467
        if (!domainInfo.backend->replaceRRSet(domainInfo.id, resourceRecord.qname, resourceRecord.qtype, vector<DNSResourceRecord>(1, resourceRecord))) {
64!
2468
          throw ApiException("Hosting backend does not support editing records.");
×
2469
        }
×
2470
      }
64✔
2471

2472
      // return old and new serials in headers
2473
      resp->headers["X-PDNS-Old-Serial"] = std::to_string(soaData.serial);
64✔
2474
      fillSOAData(resourceRecord.content, soaData);
64✔
2475
      resp->headers["X-PDNS-New-Serial"] = std::to_string(soaData.serial);
64✔
2476
    }
64✔
2477
  }
84✔
2478
  catch (...) {
152✔
2479
    domainInfo.backend->abortTransaction();
68✔
2480
    throw;
68✔
2481
  }
68✔
2482

2483
  // Rectify
2484
  DNSSECKeeper dnssecKeeper(&backend);
84✔
2485
  if (!zone_disabled && !dnssecKeeper.isPresigned(zonename) && isZoneApiRectifyEnabled(domainInfo)) {
84!
2486
    string info;
80✔
2487
    string error_msg;
80✔
2488
    if (!dnssecKeeper.rectifyZone(zonename, error_msg, info, false)) {
80!
2489
      throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
×
2490
    }
×
2491
  }
80✔
2492

2493
  domainInfo.backend->commitTransaction();
84✔
2494

2495
  DNSSECKeeper::clearCaches(zonename);
84✔
2496
  purgeAuthCaches(zonename.toString() + "$");
84✔
2497

2498
  resp->body = "";
84✔
2499
  resp->status = 204; // No Content, but indicate success
84✔
2500
}
84✔
2501

2502
static void apiServerSearchData(HttpRequest* req, HttpResponse* resp)
2503
{
21✔
2504
  string qVar = req->getvars["q"];
21✔
2505
  string sMaxVar = req->getvars["max"];
21✔
2506
  string sObjectTypeVar = req->getvars["object_type"];
21✔
2507

2508
  size_t maxEnts = 100;
21✔
2509
  size_t ents = 0;
21✔
2510

2511
  // the following types of data can be searched for using the api
2512
  enum class ObjectType
21✔
2513
  {
21✔
2514
    ALL,
21✔
2515
    ZONE,
21✔
2516
    RECORD,
21✔
2517
    COMMENT
21✔
2518
  } objectType{};
21✔
2519

2520
  if (qVar.empty()) {
21!
2521
    throw ApiException("Query q can't be blank");
×
2522
  }
×
2523
  if (!sMaxVar.empty()) {
21!
2524
    maxEnts = std::stoi(sMaxVar);
×
2525
  }
×
2526
  if (maxEnts < 1) {
21!
2527
    throw ApiException("Maximum entries must be larger than 0");
×
2528
  }
×
2529

2530
  if (sObjectTypeVar.empty() || sObjectTypeVar == "all") {
21!
2531
    objectType = ObjectType::ALL;
15✔
2532
  }
15✔
2533
  else if (sObjectTypeVar == "zone") {
6✔
2534
    objectType = ObjectType::ZONE;
3✔
2535
  }
3✔
2536
  else if (sObjectTypeVar == "record") {
3!
2537
    objectType = ObjectType::RECORD;
3✔
2538
  }
3✔
2539
  else if (sObjectTypeVar == "comment") {
×
2540
    objectType = ObjectType::COMMENT;
×
2541
  }
×
2542
  else {
×
2543
    throw ApiException("object_type must be one of the following options: all, zone, record, comment");
×
2544
  }
×
2545

2546
  SimpleMatch simpleMatch(qVar, true);
21✔
2547
  UeberBackend backend;
21✔
2548
  vector<DomainInfo> domains;
21✔
2549
  vector<DNSResourceRecord> result_rr;
21✔
2550
  vector<Comment> result_c;
21✔
2551
  map<int, DomainInfo> zoneIdZone;
21✔
2552
  map<int, DomainInfo>::iterator val;
21✔
2553
  Json::array doc;
21✔
2554

2555
  backend.getAllDomains(&domains, false, true);
21✔
2556

2557
  for (const DomainInfo& domainInfo : domains) {
1,281✔
2558
    if ((objectType == ObjectType::ALL || objectType == ObjectType::ZONE) && ents < maxEnts && simpleMatch.match(domainInfo.zone)) {
1,281!
2559
      doc.push_back(Json::object{
15✔
2560
        {"object_type", "zone"},
15✔
2561
        {"zone_id", apiZoneNameToId(domainInfo.zone)},
15✔
2562
        {"name", domainInfo.zone.toString()}});
15✔
2563
      ents++;
15✔
2564
    }
15✔
2565
    zoneIdZone[static_cast<int>(domainInfo.id)] = domainInfo; // populate cache
1,281✔
2566
  }
1,281✔
2567

2568
  if ((objectType == ObjectType::ALL || objectType == ObjectType::RECORD) && backend.searchRecords(qVar, maxEnts, result_rr)) {
21!
2569
    for (const DNSResourceRecord& resourceRecord : result_rr) {
51✔
2570
      if (resourceRecord.qtype.getCode() == 0) {
51✔
2571
        continue; // skip empty non-terminals
3✔
2572
      }
3✔
2573

2574
      auto object = Json::object{
48✔
2575
        {"object_type", "record"},
48✔
2576
        {"name", resourceRecord.qname.toString()},
48✔
2577
        {"type", resourceRecord.qtype.toString()},
48✔
2578
        {"ttl", (double)resourceRecord.ttl},
48✔
2579
        {"disabled", resourceRecord.disabled},
48✔
2580
        {"content", makeApiRecordContent(resourceRecord.qtype, resourceRecord.content)}};
48✔
2581

2582
      val = zoneIdZone.find(resourceRecord.domain_id);
48✔
2583
      if (val != zoneIdZone.end()) {
48!
2584
        object["zone_id"] = apiZoneNameToId(val->second.zone);
48✔
2585
        object["zone"] = val->second.zone.toString();
48✔
2586
      }
48✔
2587
      doc.emplace_back(object);
48✔
2588
    }
48✔
2589
  }
18✔
2590

2591
  if ((objectType == ObjectType::ALL || objectType == ObjectType::COMMENT) && backend.searchComments(qVar, maxEnts, result_c)) {
21!
2592
    for (const Comment& comment : result_c) {
15✔
2593
      auto object = Json::object{
3✔
2594
        {"object_type", "comment"},
3✔
2595
        {"name", comment.qname.toString()},
3✔
2596
        {"type", comment.qtype.toString()},
3✔
2597
        {"content", comment.content}};
3✔
2598

2599
      val = zoneIdZone.find(comment.domain_id);
3✔
2600
      if (val != zoneIdZone.end()) {
3!
2601
        object["zone_id"] = apiZoneNameToId(val->second.zone);
3✔
2602
        object["zone"] = val->second.zone.toString();
3✔
2603
      }
3✔
2604
      doc.emplace_back(object);
3✔
2605
    }
3✔
2606
  }
15✔
2607

2608
  resp->setJsonBody(doc);
21✔
2609
}
21✔
2610

2611
static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp)
2612
{
16✔
2613
  DNSName canon = apiNameToDNSName(req->getvars["domain"]);
16✔
2614

2615
  if (g_zoneCache.isEnabled()) {
16✔
2616
    DomainInfo domainInfo;
8✔
2617
    UeberBackend backend;
8✔
2618
    if (backend.getDomainInfo(canon, domainInfo, false)) {
8!
2619
      // zone exists (uncached), add/update it in the zone cache.
2620
      // Handle this first, to avoid concurrent queries re-populating the other caches.
2621
      g_zoneCache.add(domainInfo.zone, static_cast<int>(domainInfo.id));
×
2622
    }
×
2623
    else {
8✔
2624
      g_zoneCache.remove(domainInfo.zone);
8✔
2625
    }
8✔
2626
  }
8✔
2627

2628
  DNSSECKeeper::clearCaches(canon);
16✔
2629
  // purge entire zone from cache, not just zone-level records.
2630
  uint64_t count = purgeAuthCaches(canon.toString() + "$");
16✔
2631
  resp->setJsonBody(Json::object{
16✔
2632
    {"count", (int)count},
16✔
2633
    {"result", "Flushed cache."}});
16✔
2634
}
16✔
2635

2636
static std::ostream& operator<<(std::ostream& outStream, StatType statType)
2637
{
×
2638
  switch (statType) {
×
2639
  case StatType::counter:
×
2640
    return outStream << "counter";
×
2641
  case StatType::gauge:
×
2642
    return outStream << "gauge";
×
2643
  };
×
2644
  return outStream << static_cast<uint16_t>(statType);
×
2645
}
×
2646

2647
static void prometheusMetrics(HttpRequest* /* req */, HttpResponse* resp)
2648
{
×
2649
  std::ostringstream output;
×
2650
  for (const auto& metricName : S.getEntries()) {
×
2651
    // Prometheus suggest using '_' instead of '-'
2652
    std::string prometheusMetricName = "pdns_auth_" + boost::replace_all_copy(metricName, "-", "_");
×
2653

2654
    output << "# HELP " << prometheusMetricName << " " << S.getDescrip(metricName) << "\n";
×
2655
    output << "# TYPE " << prometheusMetricName << " " << S.getStatType(metricName) << "\n";
×
2656
    output << prometheusMetricName << " " << S.read(metricName) << "\n";
×
2657
  }
×
2658

2659
  output << "# HELP pdns_auth_info "
×
2660
         << "Info from PowerDNS, value is always 1"
×
2661
         << "\n";
×
2662
  output << "# TYPE pdns_auth_info "
×
2663
         << "gauge"
×
2664
         << "\n";
×
2665
  output << "pdns_auth_info{version=\"" << VERSION << "\"} "
×
2666
         << "1"
×
2667
         << "\n";
×
2668

2669
  resp->body = output.str();
×
2670
  resp->headers["Content-Type"] = "text/plain";
×
2671
  resp->status = 200;
×
2672
}
×
2673

2674
static void cssfunction(HttpRequest* /* req */, HttpResponse* resp)
2675
{
×
2676
  resp->headers["Cache-Control"] = "max-age=86400";
×
2677
  resp->headers["Content-Type"] = "text/css";
×
2678

2679
  ostringstream ret;
×
2680
  ret << "* { box-sizing: border-box; margin: 0; padding: 0; }" << endl;
×
2681
  ret << "body { color: black; background: white; margin-top: 1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; position: relative; }" << endl;
×
2682
  ret << "a { color: #0959c2; }" << endl;
×
2683
  ret << "a:hover { color: #3B8EC8; }" << endl;
×
2684
  ret << ".row { width: 940px; max-width: 100%; min-width: 768px; margin: 0 auto; }" << endl;
×
2685
  ret << ".row:before, .row:after { display: table; content:\" \"; }" << endl;
×
2686
  ret << ".row:after { clear: both; }" << endl;
×
2687
  ret << ".columns { position: relative; min-height: 1px; float: left; }" << endl;
×
2688
  ret << ".all { width: 100%; }" << endl;
×
2689
  ret << ".headl { width: 60%; }" << endl;
×
2690
  ret << ".header { width: 39.5%; float: right; background-repeat: no-repeat; margin-top: 7px; ";
×
2691
  ret << "background-image: url(data:image/png;base64,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);";
×
2692
  ret << " width: 154px; height: 20px; }" << endl;
×
2693
  ret << "a#appname { margin: 0; font-size: 27px; color: #666; text-decoration: none; font-weight: bold; display: block; }" << endl;
×
2694
  ret << "footer { border-top:  1px solid #ddd; padding-top: 4px; font-size: 12px; }" << endl;
×
2695
  ret << "footer.row { margin-top: 1em; margin-bottom: 1em; }" << endl;
×
2696
  ret << ".panel { background: #f2f2f2; border: 1px solid #e6e6e6; margin: 0 0 22px 0; padding: 20px; }" << endl;
×
2697
  ret << "table.data { width: 100%; border-spacing: 0; border-top: 1px solid #333; }" << endl;
×
2698
  ret << "table.data td { border-bottom: 1px solid #333; padding: 2px; }" << endl;
×
2699
  ret << "table.data tr:nth-child(2n) { background: #e2e2e2; }" << endl;
×
2700
  ret << "table.data tr:hover { background: white; }" << endl;
×
2701
  ret << ".ringmeta { margin-bottom: 5px; }" << endl;
×
2702
  ret << ".resetring {float: right; }" << endl;
×
2703
  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;
×
2704
  ret << ".resetring:hover i { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA2ElEQVQY013PMUoDcRDF4c+kEzxCsNNCrBQvIGhnlcYm11EkBxAraw8gglgIoiJpAoKIYlBcgrgopsma3c3fwt1k9cHA480M8xvQp/nMjorOWY5ov7IAYlpjQk7aYxcuWBpwFQgJnUcaYk7GhEDIGL5w+MVpKLIRyR2b4JOjvGhUKzHTv2W7iuSN479Dvu9plf1awbQ6y3x1sU5tjpVJcMbakF6Ycoas8Dl5xEHJ160wRdfqzXfa6XQ4PLDlicWUjxHxZfndL/N+RhiwNzl/Q6PDhn/qsl76H7prcApk2B1aAAAAAElFTkSuQmCC);}" << endl;
×
2705
  ret << ".resizering {float: right;}" << endl;
×
2706
  resp->body = ret.str();
×
2707
  resp->status = 200;
×
2708
}
×
2709

2710
void AuthWebServer::webThread()
2711
{
12✔
2712
  try {
12✔
2713
    setThreadName("pdns/webserver");
12✔
2714
    if (::arg().mustDo("api")) {
12!
2715
      d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", apiServerCacheFlush, "PUT");
12✔
2716
      d_ws->registerApiHandler("/api/v1/servers/localhost/config", apiServerConfig, "GET");
12✔
2717
      d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", apiServerSearchData, "GET");
12✔
2718
      d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", apiServerStatistics, "GET");
12✔
2719
      d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries/<ip>/<nameserver>", &apiServerAutoprimaryDetailDELETE, "DELETE");
12✔
2720
      d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesGET, "GET");
12✔
2721
      d_ws->registerApiHandler("/api/v1/servers/localhost/autoprimaries", &apiServerAutoprimariesPOST, "POST");
12✔
2722
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailGET, "GET");
12✔
2723
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailPUT, "PUT");
12✔
2724
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys/<id>", apiServerTSIGKeyDetailDELETE, "DELETE");
12✔
2725
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysGET, "GET");
12✔
2726
      d_ws->registerApiHandler("/api/v1/servers/localhost/tsigkeys", apiServerTSIGKeysPOST, "POST");
12✔
2727
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/axfr-retrieve", apiServerZoneAxfrRetrieve, "PUT");
12✔
2728
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysGET, "GET");
12✔
2729
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysPOST, "POST");
12✔
2730
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysPUT, "PUT");
12✔
2731
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys/<key_id>", apiZoneCryptokeysDELETE, "DELETE");
12✔
2732
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeysGET, "GET");
12✔
2733
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/cryptokeys", apiZoneCryptokeysPOST, "POST");
12✔
2734
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/export", apiServerZoneExport, "GET");
12✔
2735
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindGET, "GET");
12✔
2736
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindPUT, "PUT");
12✔
2737
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata/<kind>", apiZoneMetadataKindDELETE, "DELETE");
12✔
2738
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadataGET, "GET");
12✔
2739
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/metadata", apiZoneMetadataPOST, "POST");
12✔
2740
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/notify", apiServerZoneNotify, "PUT");
12✔
2741
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>/rectify", apiServerZoneRectify, "PUT");
12✔
2742
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailGET, "GET");
12✔
2743
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPATCH, "PATCH");
12✔
2744
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPUT, "PUT");
12✔
2745
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailDELETE, "DELETE");
12✔
2746
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesGET, "GET");
12✔
2747
      d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesPOST, "POST");
12✔
2748
      d_ws->registerApiHandler("/api/v1/servers/localhost", apiServerDetail, "GET");
12✔
2749
      d_ws->registerApiHandler("/api/v1/servers", apiServer, "GET");
12✔
2750
      d_ws->registerApiHandler("/api/v1", apiDiscoveryV1, "GET");
12✔
2751
      d_ws->registerApiHandler("/api/docs", apiDocs, "GET");
12✔
2752
      d_ws->registerApiHandler("/api", apiDiscovery, "GET");
12✔
2753
    }
12✔
2754
    if (::arg().mustDo("webserver")) {
12✔
2755
      d_ws->registerWebHandler(
4✔
2756
        "/style.css", [](HttpRequest* req, HttpResponse* resp) { cssfunction(req, resp); }, "GET");
4✔
2757
      d_ws->registerWebHandler(
4✔
2758
        "/", [this](HttpRequest* req, HttpResponse* resp) { indexfunction(req, resp); }, "GET");
4✔
2759
      d_ws->registerWebHandler("/metrics", prometheusMetrics, "GET");
4✔
2760
    }
4✔
2761
    d_ws->go();
12✔
2762
  }
12✔
2763
  catch (...) {
12✔
2764
    g_log << Logger::Error << "AuthWebServer thread caught an exception, dying" << endl;
×
2765
    _exit(1);
×
2766
  }
×
2767
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc