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

PowerDNS / pdns / 18743945403

23 Oct 2025 09:29AM UTC coverage: 65.845% (+0.02%) from 65.829%
18743945403

Pull #16356

github

web-flow
Merge 8a2027ef1 into efa3637e8
Pull Request #16356: auth 5.0: backport "pdnsutil: fix b2b-migrate to from sql to non-sql"

42073 of 92452 branches covered (45.51%)

Branch coverage included in aggregate %.

128008 of 165855 relevant lines covered (77.18%)

6379935.17 hits per line

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

25.03
/pdns/recursordist/ws-recursor.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

23
#include "config.h"
24

25
#include "ws-recursor.hh"
26
#include "json.hh"
27

28
#include <algorithm>
29
#include <string>
30
#include "namespaces.hh"
31
#include <iostream>
32
#include "iputils.hh"
33
#include "rec_channel.hh"
34
#include "rec_metrics.hh"
35
#include "arguments.hh"
36
#include "misc.hh"
37
#include "syncres.hh"
38
#include "dnsparser.hh"
39
#include "json11.hpp"
40
#include "webserver.hh"
41
#include "ws-api.hh"
42
#include "logging.hh"
43
#include "rec-lua-conf.hh"
44
#include "rpzloader.hh"
45
#include "rec-main.hh"
46
#include "rec-rust-lib/cxxsettings.hh" // IWYU pragma: keep, needed by included generated file
47
#include "rec-rust-lib/rust/src/bridge.hh"
48
#include "rec-rust-lib/rust/web.rs.h"
49
#include "rec-rust-lib/rust/misc.rs.h"
50

51
using json11::Json;
52

53
void productServerStatisticsFetch(map<string, string>& out)
54
{
85✔
55
  auto stats = getAllStatsMap(StatComponent::API);
85✔
56
  map<string, string> ret;
85✔
57
  for (const auto& entry : stats) {
20,400✔
58
    ret.emplace(entry.first, entry.second.d_value);
20,400✔
59
  }
20,400✔
60
  out.swap(ret);
85✔
61
}
85✔
62

63
std::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
64
{
×
65
  return getStatByName(name);
×
66
}
×
67

68
static void apiWriteConfigFile(const string& filebasename, const string& content)
69
{
×
70
  if (::arg()["api-config-dir"].empty()) {
×
71
    throw ApiException("Config Option \"api-config-dir\" must be set");
×
72
  }
×
73

74
  string filename = ::arg()["api-config-dir"] + "/" + filebasename;
×
75
  if (g_yamlSettings) {
×
76
    filename += ".yml";
×
77
  }
×
78
  else {
×
79
    filename += ".conf";
×
80
  }
×
81
  ofstream ofconf(filename);
×
82
  if (!ofconf) {
×
83
    throw ApiException("Could not open config fragment file '" + filename + "' for writing: " + stringerror());
×
84
  }
×
85
  ofconf << "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
×
86
  ofconf << content << endl;
×
87
  ofconf.close();
×
88
}
×
89

90
static void apiServerConfigACLGET(const std::string& aclType, HttpRequest* /* req */, HttpResponse* resp)
91
{
×
92
  // Return currently configured ACLs
93
  vector<string> entries;
×
94
  auto lock1 = g_initialAllowFrom.lock();
×
95
  auto lock2 = g_initialAllowNotifyFrom.lock();
×
96
  if (*lock1 && aclType == "allow-from") {
×
97
    entries = (*lock1)->toStringVector();
×
98
  }
×
99
  else if (*lock2 && aclType == "allow-notify-from") {
×
100
    entries = (*lock2)->toStringVector();
×
101
  }
×
102

103
  resp->setJsonBody(Json::object{
×
104
    {"name", aclType},
×
105
    {"value", entries},
×
106
  });
×
107
}
×
108

109
static void apiServerConfigACLPUT(const std::string& aclType, HttpRequest* req, HttpResponse* resp)
110
{
×
111
  const auto& document = req->json();
×
112

113
  const auto& jlist = document["value"];
×
114

115
  if (!jlist.is_array()) {
×
116
    throw ApiException("'value' must be an array");
×
117
  }
×
118

119
  if (g_yamlSettings) {
×
120
    ::rust::Vec<::rust::String> vec;
×
121
    for (const auto& value : jlist.array_items()) {
×
122
      vec.emplace_back(value.string_value());
×
123
    }
×
124

125
    try {
×
126
      ::pdns::rust::settings::rec::validate_allow_from(aclType, vec);
×
127
    }
×
128
    catch (const ::rust::Error& e) {
×
129
      throw ApiException(string("Unable to convert: ") + e.what());
×
130
    }
×
131
    ::rust::String yaml;
×
132
    if (aclType == "allow-from") {
×
133
      yaml = pdns::rust::settings::rec::allow_from_to_yaml_string_incoming("allow_from", "allow_from_file", vec);
×
134
    }
×
135
    else {
×
136
      yaml = pdns::rust::settings::rec::allow_from_to_yaml_string_incoming("allow_notify_from", "allow_notify_from_file", vec);
×
137
    }
×
138
    apiWriteConfigFile(aclType, string(yaml));
×
139
  }
×
140
  else {
×
141
    NetmaskGroup nmg;
×
142
    for (const auto& value : jlist.array_items()) {
×
143
      try {
×
144
        nmg.addMask(value.string_value());
×
145
      }
×
146
      catch (const NetmaskException& e) {
×
147
        throw ApiException(e.reason);
×
148
      }
×
149
    }
×
150

151
    ostringstream strStream;
×
152

153
    // Clear <foo>-from-file if set, so our changes take effect
154
    strStream << aclType << "-file=" << endl;
×
155

156
    // Clear ACL setting, and provide a "parent" value
157
    strStream << aclType << "=" << endl;
×
158
    strStream << aclType << "+=" << nmg.toString() << endl;
×
159

160
    apiWriteConfigFile(aclType, strStream.str());
×
161
  }
×
162

163
  parseACLs();
×
164

165
  apiServerConfigACLGET(aclType, req, resp);
×
166
}
×
167

168
static void apiServerConfigAllowFromGET(HttpRequest* req, HttpResponse* resp)
169
{
×
170
  apiServerConfigACLGET("allow-from", req, resp);
×
171
}
×
172

173
static void apiServerConfigAllowNotifyFromGET(HttpRequest* req, HttpResponse* resp)
174
{
×
175
  apiServerConfigACLGET("allow-notify-from", req, resp);
×
176
}
×
177

178
static void apiServerConfigAllowFromPUT(HttpRequest* req, HttpResponse* resp)
179
{
×
180
  apiServerConfigACLPUT("allow-from", req, resp);
×
181
}
×
182

183
static void apiServerConfigAllowNotifyFromPUT(HttpRequest* req, HttpResponse* resp)
184
{
×
185
  apiServerConfigACLPUT("allow-notify-from", req, resp);
×
186
}
×
187

188
static bool isAllowedNotify(DNSName qname)
189
{
×
190
  auto lock = g_initialAllowNotifyFor.lock();
×
191

192
  if (*lock == nullptr || (*lock)->empty()) {
×
193
    return false;
×
194
  }
×
195

196
  do {
×
197
    auto ret = (*lock)->find(qname);
×
198
    if (ret != (*lock)->end()) {
×
199
      return true;
×
200
    }
×
201
  } while (qname.chopOff());
×
202
  return false;
×
203
}
×
204

205
static void fillZone(const DNSName& zonename, HttpResponse* resp)
206
{
×
207
  auto lock = g_initialDomainMap.lock();
×
208
  auto iter = (*lock)->find(zonename);
×
209
  if (iter == (*lock)->end()) {
×
210
    throw ApiException("Could not find domain '" + zonename.toLogString() + "'");
×
211
  }
×
212

213
  const SyncRes::AuthDomain& zone = iter->second;
×
214

215
  Json::array servers;
×
216
  for (const ComboAddress& server : zone.d_servers) {
×
217
    servers.emplace_back(server.toStringWithPort());
×
218
  }
×
219

220
  Json::array records;
×
221
  for (const SyncRes::AuthDomain::records_t::value_type& record : zone.d_records) {
×
222
    records.push_back(Json::object{
×
223
      {"name", record.d_name.toString()},
×
224
      {"type", DNSRecordContent::NumberToType(record.d_type)},
×
225
      {"ttl", (double)record.d_ttl},
×
226
      {"content", record.getContent()->getZoneRepresentation()}});
×
227
  }
×
228

229
  // id is the canonical lookup key, which doesn't actually match the name (in some cases)
230
  string zoneId = apiZoneNameToId(iter->first);
×
231
  Json::object doc = {
×
232
    {"id", zoneId},
×
233
    {"url", "/api/v1/servers/localhost/zones/" + zoneId},
×
234
    {"name", iter->first.toString()},
×
235
    {"kind", zone.d_servers.empty() ? "Native" : "Forwarded"},
×
236
    {"servers", servers},
×
237
    {"recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward},
×
238
    {"notify_allowed", isAllowedNotify(zonename)},
×
239
    {"records", records}};
×
240

241
  resp->setJsonBody(doc);
×
242
}
×
243

244
static void doCreateZone(const Json& document)
245
{
×
246
  if (::arg()["api-config-dir"].empty()) {
×
247
    throw ApiException("Config Option \"api-config-dir\" must be set");
×
248
  }
×
249

250
  const DNSName zone = apiNameToDNSName(stringFromJson(document, "name"));
×
251
  const string zonename = zone.toString();
×
252
  apiCheckNameAllowedCharacters(zonename);
×
253

254
  string singleIPTarget = document["single_target_ip"].string_value();
×
255
  string kind = toUpper(stringFromJson(document, "kind"));
×
256
  bool rdFlag = boolFromJson(document, "recursion_desired");
×
257
  bool notifyAllowed = boolFromJson(document, "notify_allowed", false);
×
258
  string confbasename = "zone-" + apiZoneNameToId(zone);
×
259

260
  const string yamlAPIZonesFile = ::arg()["api-config-dir"] + "/apizones";
×
261

262
  if (kind == "NATIVE") {
×
263
    if (rdFlag) {
×
264
      throw ApiException("kind=Native and recursion_desired are mutually exclusive");
×
265
    }
×
266
    if (!singleIPTarget.empty()) {
×
267
      try {
×
268
        ComboAddress rem(singleIPTarget);
×
269
        if (rem.sin4.sin_family != AF_INET) {
×
270
          throw ApiException("");
×
271
        }
×
272
        singleIPTarget = rem.toString();
×
273
      }
×
274
      catch (...) {
×
275
        throw ApiException("Single IP target '" + singleIPTarget + "' is invalid");
×
276
      }
×
277
    }
×
278
    string zonefilename = ::arg()["api-config-dir"] + "/" + confbasename + ".zone";
×
279
    ofstream ofzone(zonefilename.c_str());
×
280
    if (!ofzone) {
×
281
      throw ApiException("Could not open '" + zonefilename + "' for writing: " + stringerror());
×
282
    }
×
283
    ofzone << "; Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
×
284
    ofzone << zonename << "\tIN\tSOA\tlocal.zone.\thostmaster." << zonename << " 1 1 1 1 1" << endl;
×
285
    if (!singleIPTarget.empty()) {
×
286
      ofzone << zonename << "\t3600\tIN\tA\t" << singleIPTarget << endl;
×
287
      ofzone << "*." << zonename << "\t3600\tIN\tA\t" << singleIPTarget << endl;
×
288
    }
×
289
    ofzone.close();
×
290

291
    if (g_yamlSettings) {
×
292
      pdns::rust::settings::rec::AuthZone authzone;
×
293
      authzone.zone = zonename;
×
294
      authzone.file = zonefilename;
×
295
      pdns::rust::settings::rec::api_add_auth_zone(yamlAPIZonesFile, std::move(authzone));
×
296
    }
×
297
    else {
×
298
      apiWriteConfigFile(confbasename, "auth-zones+=" + zonename + "=" + zonefilename);
×
299
    }
×
300
  }
×
301
  else if (kind == "FORWARDED") {
×
302
    if (g_yamlSettings) {
×
303
      pdns::rust::settings::rec::ForwardZone forward;
×
304
      forward.zone = zonename;
×
305
      forward.recurse = rdFlag;
×
306
      forward.notify_allowed = notifyAllowed;
×
307
      for (const auto& value : document["servers"].array_items()) {
×
308
        forward.forwarders.emplace_back(value.string_value());
×
309
      }
×
310
      pdns::rust::settings::rec::api_add_forward_zone(yamlAPIZonesFile, std::move(forward));
×
311
    }
×
312
    else {
×
313
      string serverlist;
×
314
      for (const auto& value : document["servers"].array_items()) {
×
315
        const string& server = value.string_value();
×
316
        if (server.empty()) {
×
317
          throw ApiException("Forwarded-to server must not be an empty string");
×
318
        }
×
319
        try {
×
320
          ComboAddress address = parseIPAndPort(server, 53);
×
321
          if (!serverlist.empty()) {
×
322
            serverlist += ";";
×
323
          }
×
324
          serverlist += address.toStringWithPort();
×
325
        }
×
326
        catch (const PDNSException& e) {
×
327
          throw ApiException(e.reason);
×
328
        }
×
329
      }
×
330
      if (serverlist.empty()) {
×
331
        throw ApiException("Need at least one upstream server when forwarding");
×
332
      }
×
333

334
      const string notifyAllowedConfig = notifyAllowed ? "\nallow-notify-for+=" + zonename : "";
×
335
      if (rdFlag) {
×
336
        apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename + "=" + serverlist + notifyAllowedConfig);
×
337
      }
×
338
      else {
×
339
        apiWriteConfigFile(confbasename, "forward-zones+=" + zonename + "=" + serverlist + notifyAllowedConfig);
×
340
      }
×
341
    }
×
342
  }
×
343
  else {
×
344
    throw ApiException("invalid kind");
×
345
  }
×
346
}
×
347

348
static bool doDeleteZone(const DNSName& zonename)
349
{
×
350
  if (::arg()["api-config-dir"].empty()) {
×
351
    throw ApiException("Config Option \"api-config-dir\" must be set");
×
352
  }
×
353

354
  string filename;
×
355
  if (g_yamlSettings) {
×
356
    const string yamlAPiZonesFile = ::arg()["api-config-dir"] + "/apizones";
×
357
    pdns::rust::settings::rec::api_delete_zone(yamlAPiZonesFile, zonename.toString());
×
358
  }
×
359
  else {
×
360
    // this one must exist
361
    filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf";
×
362
    if (unlink(filename.c_str()) != 0) {
×
363
      return false;
×
364
    }
×
365
  }
×
366
  // .zone file is optional
367
  filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".zone";
×
368
  unlink(filename.c_str());
×
369

370
  return true;
×
371
}
×
372

373
static void apiServerZonesPOST(HttpRequest* req, HttpResponse* resp)
374
{
×
375
  if (::arg()["api-config-dir"].empty()) {
×
376
    throw ApiException("Config Option \"api-config-dir\" must be set");
×
377
  }
×
378

379
  Json document = req->json();
×
380

381
  DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
×
382
  {
×
383
    auto map = g_initialDomainMap.lock();
×
384
    const auto& iter = (*map)->find(zonename);
×
385
    if (iter != (*map)->cend()) {
×
386
      throw ApiException("Zone already exists");
×
387
    }
×
388
    doCreateZone(document);
×
389
  }
×
390
  reloadZoneConfiguration(g_yamlSettings);
×
391
  fillZone(zonename, resp);
×
392
  resp->status = 201;
×
393
}
×
394

395
static void apiServerZonesGET(HttpRequest* /* req */, HttpResponse* resp)
396
{
×
397
  Json::array doc;
×
398
  auto lock = g_initialDomainMap.lock();
×
399
  for (const auto& val : **lock) {
×
400
    const SyncRes::AuthDomain& zone = val.second;
×
401
    Json::array servers;
×
402
    for (const auto& server : zone.d_servers) {
×
403
      servers.emplace_back(server.toStringWithPort());
×
404
    }
×
405
    // id is the canonical lookup key, which doesn't actually match the name (in some cases)
406
    string zoneId = apiZoneNameToId(val.first);
×
407
    doc.push_back(Json::object{
×
408
      {"id", zoneId},
×
409
      {"url", "/api/v1/servers/localhost/zones/" + zoneId},
×
410
      {"name", val.first.toString()},
×
411
      {"kind", zone.d_servers.empty() ? "Native" : "Forwarded"},
×
412
      {"servers", servers},
×
413
      {"recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward}});
×
414
  }
×
415
  resp->setJsonBody(doc);
×
416
}
×
417

418
static inline DNSName findZoneById(HttpRequest* req)
419
{
×
420
  auto zonename = apiZoneIdToName(req->parameters["id"]);
×
421
  auto lock = g_initialDomainMap.lock();
×
422
  if ((*lock)->find(zonename) == (*lock)->end()) {
×
423
    throw ApiException("Could not find domain '" + zonename.toLogString() + "'");
×
424
  }
×
425
  return zonename;
×
426
}
×
427

428
static void apiServerZoneDetailPUT(HttpRequest* req, HttpResponse* resp)
429
{
×
430
  auto zonename = findZoneById(req);
×
431
  const auto& document = req->json();
×
432

433
  doDeleteZone(zonename);
×
434
  doCreateZone(document);
×
435
  reloadZoneConfiguration(g_yamlSettings);
×
436
  resp->body = "";
×
437
  resp->status = 204; // No Content, but indicate success
×
438
}
×
439

440
static void apiServerZoneDetailDELETE(HttpRequest* req, HttpResponse* resp)
441
{
×
442
  auto zonename = findZoneById(req);
×
443
  if (!doDeleteZone(zonename)) {
×
444
    throw ApiException("Deleting domain failed");
×
445
  }
×
446

447
  reloadZoneConfiguration(g_yamlSettings);
×
448
  // empty body on success
449
  resp->body = "";
×
450
  resp->status = 204; // No Content: declare that the zone is gone now
×
451
}
×
452

453
static void apiServerZoneDetailGET(HttpRequest* req, HttpResponse* resp)
454
{
×
455
  auto zonename = findZoneById(req);
×
456
  fillZone(zonename, resp);
×
457
}
×
458

459
static void apiServerSearchData(HttpRequest* req, HttpResponse* resp)
460
{
×
461
  string qVar = req->getvars["q"];
×
462
  if (qVar.empty()) {
×
463
    throw ApiException("Query q can't be blank");
×
464
  }
×
465

466
  auto lock = g_initialDomainMap.lock();
×
467
  Json::array doc;
×
468
  for (const SyncRes::domainmap_t::value_type& val : **lock) {
×
469
    string zoneId = apiZoneNameToId(val.first);
×
470
    string zoneName = val.first.toString();
×
471
    if (pdns_ci_find(zoneName, qVar) != string::npos) {
×
472
      doc.push_back(Json::object{
×
473
        {"type", "zone"},
×
474
        {"zone_id", zoneId},
×
475
        {"name", zoneName}});
×
476
    }
×
477

478
    // if zone name is an exact match, don't bother with returning all records/comments in it
479
    if (val.first == DNSName(qVar)) {
×
480
      continue;
×
481
    }
×
482

483
    const SyncRes::AuthDomain& zone = val.second;
×
484

485
    for (const SyncRes::AuthDomain::records_t::value_type& resourceRec : zone.d_records) {
×
486
      if (pdns_ci_find(resourceRec.d_name.toString(), qVar) == string::npos && pdns_ci_find(resourceRec.getContent()->getZoneRepresentation(), qVar) == string::npos) {
×
487
        continue;
×
488
      }
×
489

490
      doc.push_back(Json::object{
×
491
        {"type", "record"},
×
492
        {"zone_id", zoneId},
×
493
        {"zone_name", zoneName},
×
494
        {"name", resourceRec.d_name.toString()},
×
495
        {"content", resourceRec.getContent()->getZoneRepresentation()}});
×
496
    }
×
497
  }
×
498
  resp->setJsonBody(doc);
×
499
}
×
500

501
static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp)
502
{
×
503
  DNSName canon = apiNameToDNSName(req->getvars["domain"]);
×
504
  bool subtree = req->getvars.count("subtree") > 0 && req->getvars["subtree"] == "true";
×
505
  uint16_t qtype = 0xffff;
×
506
  if (req->getvars.count("type") != 0) {
×
507
    qtype = QType::chartocode(req->getvars["type"].c_str());
×
508
  }
×
509

510
  struct WipeCacheResult res = wipeCaches(canon, subtree, qtype);
×
511
  resp->setJsonBody(Json::object{
×
512
    {"count", res.record_count + res.packet_count + res.negative_record_count},
×
513
    {"result", "Flushed cache."}});
×
514
}
×
515

516
static void apiServerRPZStats(HttpRequest* /* req */, HttpResponse* resp)
517
{
13✔
518
  auto luaconf = g_luaconfs.getLocal();
13✔
519
  auto numZones = luaconf->dfe.size();
13✔
520

521
  Json::object ret;
13✔
522

523
  for (size_t i = 0; i < numZones; i++) {
26✔
524
    auto zone = luaconf->dfe.getZone(i);
13✔
525
    if (zone == nullptr) {
13!
526
      continue;
×
527
    }
×
528
    const auto& name = zone->getName();
13✔
529
    auto stats = getRPZZoneStats(name);
13✔
530
    if (stats == nullptr) {
13!
531
      continue;
×
532
    }
×
533
    Json::object zoneInfo = {
13✔
534
      {"transfers_failed", (double)stats->d_failedTransfers},
13✔
535
      {"transfers_success", (double)stats->d_successfulTransfers},
13✔
536
      {"transfers_full", (double)stats->d_fullTransfers},
13✔
537
      {"records", (double)stats->d_numberOfRecords},
13✔
538
      {"last_update", (double)stats->d_lastUpdate},
13✔
539
      {"serial", (double)stats->d_serial},
13✔
540
    };
13✔
541
    ret[name] = zoneInfo;
13✔
542
  }
13✔
543
  resp->setJsonBody(ret);
13✔
544
}
13✔
545

546
static void prometheusMetrics(HttpRequest* /* req */, HttpResponse* resp)
547
{
2✔
548
  static MetricDefinitionStorage s_metricDefinitions;
2✔
549

550
  std::ostringstream output;
2✔
551

552
  // Argument controls disabling of any stats. So
553
  // stats-api-disabled-list will be used to block returned stats.
554
  auto varmap = getAllStatsMap(StatComponent::API);
2✔
555
  for (const auto& tup : varmap) {
480✔
556
    std::string metricName = tup.first;
480✔
557
    std::string prometheusMetricName = tup.second.d_prometheusName;
480✔
558
    std::string helpname = tup.second.d_prometheusName;
480✔
559
    MetricDefinition metricDetails;
480✔
560

561
    if (s_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
480✔
562
      std::string prometheusTypeName = MetricDefinitionStorage::getPrometheusStringMetricType(
340✔
563
        metricDetails.d_prometheusType);
340✔
564

565
      if (prometheusTypeName.empty()) {
340!
566
        continue;
×
567
      }
×
568
      if (metricDetails.d_prometheusType == PrometheusMetricType::multicounter) {
340✔
569
        helpname = prometheusMetricName.substr(0, prometheusMetricName.find('{'));
6✔
570
      }
6✔
571
      else if (metricDetails.d_prometheusType == PrometheusMetricType::histogram) {
334✔
572
        helpname = prometheusMetricName.substr(0, prometheusMetricName.find('{'));
4✔
573
        // name is XXX_count, strip the _count part
574
        helpname = helpname.substr(0, helpname.length() - 6);
4✔
575
      }
4✔
576
      output << "# TYPE " << helpname << " " << prometheusTypeName << "\n";
340✔
577
      output << "# HELP " << helpname << " " << metricDetails.d_description << "\n";
340✔
578
    }
340✔
579
    output << prometheusMetricName << " " << tup.second.d_value << "\n";
480✔
580
  }
480✔
581

582
  output << "# HELP pdns_recursor_info "
2✔
583
         << "Info from pdns_recursor, value is always 1"
2✔
584
         << "\n";
2✔
585
  output << "# TYPE pdns_recursor_info "
2✔
586
         << "gauge"
2✔
587
         << "\n";
2✔
588
  output << "pdns_recursor_info{version=\"" << VERSION << "\"} "
2✔
589
         << "1"
2✔
590
         << "\n";
2✔
591

592
  resp->body = output.str();
2✔
593
  resp->headers["Content-Type"] = "text/plain; version=0.0.4";
2✔
594
  resp->status = 200;
2✔
595
}
2✔
596

597
#include "htmlfiles.h"
598

599
static void serveStuff(HttpRequest* req, HttpResponse* resp)
600
{
×
601
  resp->headers["Cache-Control"] = "max-age=86400";
×
602

603
  if (req->url.path == "/") {
×
604
    req->url.path = "/index.html";
×
605
  }
×
606

607
  const string charset = "; charset=utf-8";
×
608
  if (boost::ends_with(req->url.path, ".html")) {
×
609
    resp->headers["Content-Type"] = "text/html" + charset;
×
610
  }
×
611
  else if (boost::ends_with(req->url.path, ".css")) {
×
612
    resp->headers["Content-Type"] = "text/css" + charset;
×
613
  }
×
614
  else if (boost::ends_with(req->url.path, ".js")) {
×
615
    resp->headers["Content-Type"] = "application/javascript" + charset;
×
616
  }
×
617
  else if (boost::ends_with(req->url.path, ".png")) {
×
618
    resp->headers["Content-Type"] = "image/png";
×
619
  }
×
620

621
  resp->headers["X-Content-Type-Options"] = "nosniff";
×
622
  resp->headers["X-Frame-Options"] = "deny";
×
623
  resp->headers["X-Permitted-Cross-Domain-Policies"] = "none";
×
624

625
  resp->headers["X-XSS-Protection"] = "1; mode=block";
×
626
  //  resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
627

628
  if (!req->url.path.empty() && (g_urlmap.count(req->url.path.substr(1)) != 0)) {
×
629
    resp->body = g_urlmap.at(req->url.path.substr(1));
×
630
    resp->status = 200;
×
631
  }
×
632
  else {
×
633
    resp->status = 404;
×
634
  }
×
635
}
×
636

637
const std::map<std::string, MetricDefinition> MetricDefinitionStorage::d_metrics = {
638
#include "rec-prometheus-gen.h"
639
};
640

641
#ifndef RUST_WS
642

643
constexpr bool CHECK_PROMETHEUS_METRICS = false;
644

645
static void validatePrometheusMetrics()
646
{
647
  MetricDefinitionStorage s_metricDefinitions;
648

649
  auto varmap = getAllStatsMap(StatComponent::API);
650
  for (const auto& tup : varmap) {
651
    std::string metricName = tup.first;
652
    // A few special cases not handled correctly by the check below
653
    if (metricName.find("cpu-msec-") == 0) {
654
      continue;
655
    }
656
    if (metricName.find("cumul-") == 0) {
657
      continue;
658
    }
659
    if (metricName.find("auth-") == 0 && metricName.find("-answers") != string::npos) {
660
      continue;
661
    }
662
    if (metricName.find("proxy-mapping-total") == 0) {
663
      continue;
664
    }
665
    MetricDefinition metricDetails;
666

667
    if (!s_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
668
      SLOG(g_log << Logger::Debug << "{ \"" << metricName << "\", MetricDefinition(PrometheusMetricType::counter, \"\")}," << endl,
669
           g_slog->info(Logr::Debug, "{ \"" + metricName + "\", MetricDefinition(PrometheusMetricType::counter, \"\")},"));
670
    }
671
  }
672
}
673

674
RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
675
{
676
  if (CHECK_PROMETHEUS_METRICS) {
677
    validatePrometheusMetrics();
678
  }
679

680
  d_ws = make_unique<AsyncWebServer>(fdm, arg()["webserver-address"], arg().asNum("webserver-port"));
681
  d_ws->setSLog(g_slog->withName("webserver"));
682

683
  d_ws->setApiKey(arg()["api-key"], arg().mustDo("webserver-hash-plaintext-credentials"));
684
  d_ws->setPassword(arg()["webserver-password"], arg().mustDo("webserver-hash-plaintext-credentials"));
685
  d_ws->setLogLevel(arg()["webserver-loglevel"]);
686

687
  NetmaskGroup acl;
688
  acl.toMasks(::arg()["webserver-allow-from"]);
689
  d_ws->setACL(acl);
690

691
  d_ws->bind();
692

693
  // legacy dispatch
694
  d_ws->registerApiHandler(
695
    "/jsonstat", [](HttpRequest* req, HttpResponse* resp) { jsonstat(req, resp); }, "GET", true);
696
  d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", apiServerCacheFlush, "PUT");
697
  d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-from", apiServerConfigAllowFromPUT, "PUT");
698
  d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-from", apiServerConfigAllowFromGET, "GET");
699
  d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-notify-from", apiServerConfigAllowNotifyFromGET, "GET");
700
  d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-notify-from", apiServerConfigAllowNotifyFromPUT, "PUT");
701
  d_ws->registerApiHandler("/api/v1/servers/localhost/config", apiServerConfig, "GET");
702
  d_ws->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", apiServerRPZStats, "GET");
703
  d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", apiServerSearchData, "GET");
704
  d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", apiServerStatistics, "GET", true);
705
  d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailGET, "GET");
706
  d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailPUT, "PUT");
707
  d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", apiServerZoneDetailDELETE, "DELETE");
708
  d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesGET, "GET");
709
  d_ws->registerApiHandler("/api/v1/servers/localhost/zones", apiServerZonesPOST, "POST");
710
  d_ws->registerApiHandler("/api/v1/servers/localhost", apiServerDetail, "GET", true);
711
  d_ws->registerApiHandler("/api/v1/servers", apiServer, "GET");
712
  d_ws->registerApiHandler("/api/v1", apiDiscoveryV1, "GET");
713
  d_ws->registerApiHandler("/api", apiDiscovery, "GET");
714

715
  for (const auto& url : g_urlmap) {
716
    d_ws->registerWebHandler("/" + url.first, serveStuff, "GET");
717
  }
718

719
  d_ws->registerWebHandler("/", serveStuff, "GET");
720
  d_ws->registerWebHandler("/metrics", prometheusMetrics, "GET");
721
  d_ws->go();
722
}
723
#endif // !RUST_WS
724

725
static void jsonstat(HttpRequest* req, HttpResponse* resp)
726
{
4✔
727
  string command;
4✔
728

729
  if (req->getvars.count("command") != 0) {
4!
730
    command = req->getvars["command"];
4✔
731
    req->getvars.erase("command");
4✔
732
  }
4✔
733

734
  map<string, string> stats;
4✔
735
  if (command == "get-query-ring") {
4!
736
    typedef pair<DNSName, uint16_t> query_t;
×
737
    vector<query_t> queries;
×
738
    bool filter = !req->getvars["public-filtered"].empty();
×
739

740
    if (req->getvars["name"] == "servfail-queries") {
×
741
      queries = broadcastAccFunction<vector<query_t>>(pleaseGetServfailQueryRing);
×
742
    }
×
743
    else if (req->getvars["name"] == "bogus-queries") {
×
744
      queries = broadcastAccFunction<vector<query_t>>(pleaseGetBogusQueryRing);
×
745
    }
×
746
    else if (req->getvars["name"] == "queries") {
×
747
      queries = broadcastAccFunction<vector<query_t>>(pleaseGetQueryRing);
×
748
    }
×
749

750
    typedef map<query_t, unsigned int> counts_t;
×
751
    counts_t counts;
×
752
    for (const query_t& count : queries) {
×
753
      if (filter) {
×
754
        counts[pair(getRegisteredName(count.first), count.second)]++;
×
755
      }
×
756
      else {
×
757
        counts[pair(count.first, count.second)]++;
×
758
      }
×
759
    }
×
760

761
    typedef std::multimap<int, query_t> rcounts_t;
×
762
    rcounts_t rcounts;
×
763

764
    for (const auto& count : counts) {
×
765
      rcounts.emplace(-count.second, count.first);
×
766
    }
×
767

768
    Json::array entries;
×
769
    unsigned int tot = 0;
×
770
    unsigned int totIncluded = 0;
×
771
    for (const rcounts_t::value_type& count : rcounts) {
×
772
      totIncluded -= count.first;
×
773
      entries.push_back(Json::array{
×
774
        -count.first, count.second.first.toLogString(), DNSRecordContent::NumberToType(count.second.second)});
×
775
      if (tot++ >= 100) {
×
776
        break;
×
777
      }
×
778
    }
×
779
    if (queries.size() != totIncluded) {
×
780
      entries.push_back(Json::array{
×
781
        (int)(queries.size() - totIncluded), "", ""});
×
782
    }
×
783
    resp->setJsonBody(Json::object{{"entries", entries}});
×
784
    return;
×
785
  }
×
786
  if (command == "get-remote-ring") {
4!
787
    vector<ComboAddress> queries;
4✔
788
    if (req->getvars["name"] == "remotes") {
4!
789
      queries = broadcastAccFunction<vector<ComboAddress>>(pleaseGetRemotes);
4✔
790
    }
4✔
791
    else if (req->getvars["name"] == "servfail-remotes") {
×
792
      queries = broadcastAccFunction<vector<ComboAddress>>(pleaseGetServfailRemotes);
×
793
    }
×
794
    else if (req->getvars["name"] == "bogus-remotes") {
×
795
      queries = broadcastAccFunction<vector<ComboAddress>>(pleaseGetBogusRemotes);
×
796
    }
×
797
    else if (req->getvars["name"] == "large-answer-remotes") {
×
798
      queries = broadcastAccFunction<vector<ComboAddress>>(pleaseGetLargeAnswerRemotes);
×
799
    }
×
800
    else if (req->getvars["name"] == "timeouts") {
×
801
      queries = broadcastAccFunction<vector<ComboAddress>>(pleaseGetTimeouts);
×
802
    }
×
803
    typedef map<ComboAddress, unsigned int, ComboAddress::addressOnlyLessThan> counts_t;
4✔
804
    counts_t counts;
4✔
805
    for (const ComboAddress& query : queries) {
58✔
806
      counts[query]++;
58✔
807
    }
58✔
808

809
    typedef std::multimap<int, ComboAddress> rcounts_t;
4✔
810
    rcounts_t rcounts;
4✔
811

812
    for (const auto& count : counts) {
20✔
813
      rcounts.emplace(-count.second, count.first);
20✔
814
    }
20✔
815

816
    Json::array entries;
4✔
817
    unsigned int tot = 0;
4✔
818
    unsigned int totIncluded = 0;
4✔
819
    for (const rcounts_t::value_type& count : rcounts) {
20✔
820
      totIncluded -= count.first;
20✔
821
      entries.push_back(Json::array{
20✔
822
        -count.first, count.second.toString()});
20✔
823
      if (tot++ >= 100) {
20!
824
        break;
×
825
      }
×
826
    }
20✔
827
    if (queries.size() != totIncluded) {
4!
828
      entries.push_back(Json::array{
×
829
        (int)(queries.size() - totIncluded), ""});
×
830
    }
×
831

832
    resp->setJsonBody(Json::object{{"entries", entries}});
4✔
833
    return;
4✔
834
  }
4✔
835
  resp->setErrorResult("Command '" + command + "' not found", 404);
×
836
}
×
837

838
#ifndef RUST_WS
839

840
void AsyncServerNewConnectionMT(void* arg)
841
{
842
  auto* server = static_cast<AsyncServer*>(arg);
843

844
  try {
845
    auto socket = server->accept(); // this is actually a shared_ptr
846
    if (socket) {
847
      server->d_asyncNewConnectionCallback(socket);
848
    }
849
  }
850
  catch (NetworkError& e) {
851
    // we're running in a shared process/thread, so can't just terminate/abort.
852
    SLOG(g_log << Logger::Warning << "Network error in web thread: " << e.what() << endl,
853
         g_slog->withName("webserver")->error(Logr::Warning, e.what(), "Exception in web tread", Logging::Loggable("NetworkError")));
854
    return;
855
  }
856
  catch (...) {
857
    SLOG(g_log << Logger::Warning << "Unknown error in web thread" << endl,
858
         g_slog->withName("webserver")->info(Logr::Warning, "Exception in web tread"));
859

860
    return;
861
  }
862
}
863

864
void AsyncServer::asyncWaitForConnections(FDMultiplexer* fdm, const newconnectioncb_t& callback)
865
{
866
  d_asyncNewConnectionCallback = callback;
867
  fdm->addReadFD(d_server_socket.getHandle(), [this](int, boost::any&) { newConnection(); });
868
}
869

870
void AsyncServer::newConnection()
871
{
872
  getMT()->makeThread(&AsyncServerNewConnectionMT, this);
873
}
874

875
// This is an entry point from FDM, so it needs to catch everything.
876
void AsyncWebServer::serveConnection(const std::shared_ptr<Socket>& socket) const // NOLINT(readability-function-cognitive-complexity) #12791 Remove NOLINT(readability-function-cognitive-complexity) omoerbeek
877
{
878
  if (!socket->acl(d_acl)) {
879
    return;
880
  }
881

882
  const auto unique = getUniqueID();
883
  const string logprefix = d_logprefix + to_string(unique) + " ";
884

885
  HttpRequest req(logprefix);
886
  HttpResponse resp;
887
#ifdef RECURSOR
888
  auto log = d_slog->withValues("uniqueid", Logging::Loggable(to_string(unique)));
889
  req.setSLog(log);
890
  resp.setSLog(log);
891
#endif
892

893
  ComboAddress remote;
894
  PacketBuffer reply;
895

896
  try {
897
    YaHTTP::AsyncRequestLoader yarl;
898
    yarl.initialize(&req);
899
    socket->setNonBlocking();
900

901
    const struct timeval timeout{
902
      g_networkTimeoutMsec / 1000, static_cast<suseconds_t>(g_networkTimeoutMsec) % 1000 * 1000};
903
    std::shared_ptr<TLSCtx> tlsCtx{nullptr};
904
    if (d_loglevel > WebServer::LogLevel::None) {
905
      socket->getRemote(remote);
906
    }
907
    auto handler = std::make_shared<TCPIOHandler>("", false, socket->releaseHandle(), timeout, tlsCtx);
908

909
    PacketBuffer data;
910
    try {
911
      while (!req.complete) {
912
        auto ret = arecvtcp(data, 16384, handler, true);
913
        if (ret == LWResult::Result::Success) {
914
          string str(reinterpret_cast<const char*>(data.data()), data.size()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): safe cast, data.data() returns unsigned char *
915
          req.complete = yarl.feed(str);
916
        }
917
        else {
918
          // read error OR EOF
919
          break;
920
        }
921
      }
922
      yarl.finalize();
923
    }
924
    catch (YaHTTP::ParseError& e) {
925
      // request stays incomplete
926
      SLOG(g_log << Logger::Warning << logprefix << "Unable to parse request: " << e.what() << endl,
927
           req.d_slog->error(Logr::Warning, e.what(), "Unable to parse request"));
928
    }
929

930
    if (!validURL(req.url)) {
931
      throw PDNSException("Received request with invalid URL");
932
    }
933
    logRequest(req, remote);
934

935
    WebServer::handleRequest(req, resp);
936
    ostringstream stringStream;
937
    resp.write(stringStream);
938
    const string& str = stringStream.str();
939
    reply.insert(reply.end(), str.cbegin(), str.cend());
940

941
    logResponse(resp, remote, logprefix);
942

943
    // now send the reply
944
    if (asendtcp(reply, handler) != LWResult::Result::Success || reply.empty()) {
945
      SLOG(g_log << Logger::Error << logprefix << "Failed sending reply to HTTP client" << endl,
946
           req.d_slog->info(Logr::Error, "Failed sending reply to HTTP client"));
947
    }
948
    handler->close(); // needed to signal "done" to client
949
    if (d_loglevel >= WebServer::LogLevel::Normal) {
950
      SLOG(g_log << Logger::Notice << logprefix << remote << " \"" << req.method << " " << req.url.path << " HTTP/" << req.versionStr(req.version) << "\" " << resp.status << " " << reply.size() << endl,
951
           req.d_slog->info(Logr::Info, "Request", "remote", Logging::Loggable(remote), "method", Logging::Loggable(req.method),
952
                            "urlpath", Logging::Loggable(req.url.path), "HTTPVersion", Logging::Loggable(req.versionStr(req.version)),
953
                            "status", Logging::Loggable(resp.status), "respsize", Logging::Loggable(reply.size())));
954
    }
955
  }
956
  catch (PDNSException& e) {
957
    SLOG(g_log << Logger::Error << logprefix << "Exception: " << e.reason << endl,
958
         req.d_slog->error(Logr::Error, e.reason, "Exception handing request", "exception", Logging::Loggable("PDNSException")));
959
  }
960
  catch (std::exception& e) {
961
    if (strstr(e.what(), "timeout") == nullptr) {
962
      SLOG(g_log << Logger::Error << logprefix << "STL Exception: " << e.what() << endl,
963
           req.d_slog->error(Logr::Error, e.what(), "Exception handing request", "exception", Logging::Loggable("std::exception")));
964
    }
965
  }
966
  catch (...) {
967
    SLOG(g_log << Logger::Error << logprefix << "Unknown exception" << endl,
968
         req.d_slog->error(Logr::Error, "Exception handing request"));
969
  }
970
}
971

972
void AsyncWebServer::go()
973
{
974
  if (!d_server) {
975
    return;
976
  }
977
  auto server = std::dynamic_pointer_cast<AsyncServer>(d_server);
978
  if (!server) {
979
    return;
980
  }
981
  server->asyncWaitForConnections(d_fdm, [this](const std::shared_ptr<Socket>& socket) { serveConnection(socket); });
982
}
983
#endif // !RUST_WS
984

985
void serveRustWeb()
986
{
20✔
987
  ::rust::Vec<pdns::rust::web::rec::IncomingWSConfig> config;
20✔
988

989
  if (g_yamlSettings) {
20✔
990
    auto settings = g_yamlStruct.lock();
3✔
991
    for (const auto& listen : settings->webservice.listen) {
3✔
992
      pdns::rust::web::rec::IncomingWSConfig tmp;
1✔
993
      for (const auto& address : listen.addresses) {
1✔
994
        tmp.addresses.emplace_back(address);
1✔
995
      }
1✔
996
      tmp.tls = pdns::rust::web::rec::IncomingTLS{listen.tls.certificate, listen.tls.key};
1✔
997
      config.emplace_back(tmp);
1✔
998
    }
1✔
999
  }
3✔
1000
  if (config.empty()) {
20✔
1001
    auto address = ComboAddress(arg()["webserver-address"], arg().asNum("webserver-port"));
19✔
1002
    pdns::rust::web::rec::IncomingWSConfig tmp{{::rust::String{address.toStringWithPort()}}, {}};
19✔
1003
    config.emplace_back(tmp);
19✔
1004
  }
19✔
1005

1006
  auto passwordString = arg()["webserver-password"];
20✔
1007
  std::unique_ptr<CredentialsHolder> password;
20✔
1008
  if (!passwordString.empty()) {
20!
1009
    password = std::make_unique<CredentialsHolder>(std::move(passwordString), arg().mustDo("webserver-hash-plaintext-credentials"));
20✔
1010
  }
20✔
1011
  auto apikeyString = arg()["api-key"];
20✔
1012
  std::unique_ptr<CredentialsHolder> apikey;
20✔
1013
  if (!apikeyString.empty()) {
20!
1014
    apikey = std::make_unique<CredentialsHolder>(std::move(apikeyString), arg().mustDo("webserver-hash-plaintext-credentials"));
20✔
1015
  }
20✔
1016
  NetmaskGroup acl;
20✔
1017
  acl.toMasks(::arg()["webserver-allow-from"]);
20✔
1018
  auto aclPtr = std::make_unique<pdns::rust::misc::NetmaskGroup>(acl);
20✔
1019

1020
  auto logPtr = g_slog->withName("webserver");
20✔
1021

1022
  pdns::rust::misc::LogLevel loglevel = pdns::rust::misc::LogLevel::Normal;
20✔
1023
  const auto& configLevel = ::arg()["webserver-loglevel"];
20✔
1024
  if (configLevel == "none") {
20!
1025
    loglevel = pdns::rust::misc::LogLevel::Normal;
×
1026
  }
×
1027
  else if (configLevel == "detailed") {
20!
1028
    loglevel = pdns::rust::misc::LogLevel::Detailed;
×
1029
  }
×
1030
  // This function returns after having created the web server object that handles the requests.
1031
  // That object and its runtime are associated with a Posix thread that waits until all tasks are
1032
  // done, which normally never happens. See rec-rust-lib/rust/src/web.rs for details
1033
  pdns::rust::web::rec::serveweb(config, std::move(password), std::move(apikey), std::move(aclPtr), std::move(logPtr), loglevel);
20✔
1034
}
20✔
1035

1036
static void fromCxxToRust(const HttpResponse& cxxresp, pdns::rust::web::rec::Response& rustResponse)
1037
{
104✔
1038
  if (cxxresp.status != 0) {
104✔
1039
    rustResponse.status = cxxresp.status;
2✔
1040
  }
2✔
1041
  rustResponse.body = ::rust::Vec<::rust::u8>();
104✔
1042
  rustResponse.body.reserve(cxxresp.body.size());
104✔
1043
  std::copy(cxxresp.body.cbegin(), cxxresp.body.cend(), std::back_inserter(rustResponse.body));
104✔
1044
  for (const auto& header : cxxresp.headers) {
104✔
1045
    rustResponse.headers.emplace_back(pdns::rust::web::rec::KeyValue{header.first, header.second});
104✔
1046
  }
104✔
1047
}
104✔
1048

1049
// Convert what we receive from Rust into C++ data, call functions and convert results back to Rust data
1050
static void rustWrapper(const std::function<void(HttpRequest*, HttpResponse*)>& func, const pdns::rust::web::rec::Request& rustRequest, pdns::rust::web::rec::Response& rustResponse)
1051
{
104✔
1052
  HttpRequest request;
104✔
1053
  HttpResponse response;
104✔
1054
  request.body = std::string(reinterpret_cast<const char*>(rustRequest.body.data()), rustRequest.body.size()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
104✔
1055
  request.url = std::string(rustRequest.uri);
104✔
1056
  for (const auto& [key, value] : rustRequest.vars) {
104✔
1057
    request.getvars[std::string(key)] = std::string(value);
8✔
1058
  }
8✔
1059
  for (const auto& [key, value] : rustRequest.parameters) {
104!
1060
    request.parameters[std::string(key)] = std::string(value);
×
1061
  }
×
1062
  // These two log objects are not used by the Rust code, as they take the logging object from the
1063
  // context, initialized from an argument to pdns::rust::web::rec::serveweb()
1064
  request.d_slog = g_slog;
104✔
1065
  response.d_slog = g_slog;
104✔
1066
  try {
104✔
1067
    func(&request, &response);
104✔
1068
  }
104✔
1069
  catch (HttpException& e) {
104✔
1070
    response.body = e.response().body;
×
1071
    response.status = e.response().status;
×
1072
  }
×
1073
  catch (const ApiException& e) {
104✔
1074
    response.setErrorResult(e.what(), 422);
×
1075
  }
×
1076
  catch (const JsonException& e) {
104✔
1077
    response.setErrorResult(e.what(), 422);
×
1078
  }
×
1079
  fromCxxToRust(response, rustResponse);
104✔
1080
}
104✔
1081

1082
namespace pdns::rust::web::rec
1083
{
1084

1085
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
1086
#define WRAPPER(A) \
1087
  void A(const Request& rustRequest, Response& rustResponse) { rustWrapper(::A, rustRequest, rustResponse); }
104✔
1088

1089
WRAPPER(jsonstat)
1090
WRAPPER(apiDiscovery)
1091
WRAPPER(apiDiscoveryV1)
1092
WRAPPER(apiServer)
1093
WRAPPER(apiServerCacheFlush)
1094
WRAPPER(apiServerConfig)
1095
WRAPPER(apiServerConfigAllowFromGET)
1096
WRAPPER(apiServerConfigAllowFromPUT)
1097
WRAPPER(apiServerConfigAllowNotifyFromGET)
1098
WRAPPER(apiServerConfigAllowNotifyFromPUT)
1099
WRAPPER(apiServerDetail)
1100
WRAPPER(apiServerRPZStats)
1101
WRAPPER(apiServerSearchData)
1102
WRAPPER(apiServerStatistics)
1103
WRAPPER(apiServerZoneDetailDELETE)
1104
WRAPPER(apiServerZoneDetailGET)
1105
WRAPPER(apiServerZoneDetailPUT)
1106
WRAPPER(apiServerZonesGET)
1107
WRAPPER(apiServerZonesPOST)
1108
WRAPPER(prometheusMetrics)
1109
WRAPPER(serveStuff)
1110

1111
}
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