]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Implement Lua custom web endpoints
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 5 Nov 2020 14:24:50 +0000 (15:24 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 5 Nov 2020 14:24:50 +0000 (15:24 +0100)
13 files changed:
pdns/dnsdist-lua.cc
pdns/dnsdist-lua.hh
pdns/dnsdist-web.cc
pdns/dnsdist.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-lua-web.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-web.hh
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/docs/reference/index.rst
pdns/dnsdistdist/docs/reference/web.rst [new file with mode: 0644]
pdns/dnsdistdist/incfiles
pdns/sstuff.hh
regression-tests.dnsdist/test_API.py

index b16d466c873e3815260a1406d806f6b19b1a6fc7..16276d1b103354a4e52192c1b70749e2315b3c74 100644 (file)
@@ -2332,6 +2332,7 @@ vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool
   setupLuaInspection(luaCtx);
   setupLuaRules(luaCtx);
   setupLuaVars(luaCtx);
+  setupLuaWeb(luaCtx);
 
 #ifdef LUAJIT_VERSION
   luaCtx.executeCode(getLuaFFIWrappers());
index 4818345e650b0ecbb6916e08b3c19d0da420e749..97f2e1d0de3fa48335243d01e660cb2dc6f63a52 100644 (file)
@@ -105,4 +105,5 @@ void setupLuaBindingsProtoBuf(LuaContext& luaCtx, bool client, bool configCheck)
 void setupLuaRules(LuaContext& luaCtx);
 void setupLuaInspection(LuaContext& luaCtx);
 void setupLuaVars(LuaContext& luaCtx);
+void setupLuaWeb(LuaContext& luaCtx);
 void setupLuaLoadBalancingContext(LuaContext& luaCtx);
index 00da6abe1f89aff2ad5f48f3fb89cf3760b87d5b..a5d406a408caa1898b8e5a934e67ca34b3235ba3 100644 (file)
@@ -307,917 +307,972 @@ static json11::Json::array someResponseRulesToJson(GlobalStateHolder<vector<T>>*
   return responseRules;
 }
 
-static void connectionThread(int sock, ComboAddress remote)
+using namespace json11;
+
+static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
 {
-  setThreadName("dnsdist/webConn");
+  handleCORS(req, resp);
+  resp.status = 200;
+
+  std::ostringstream output;
+  static const std::set<std::string> metricBlacklist = { "latency-count", "latency-sum" };
+  for (const auto& e : g_stats.entries) {
+    if (e.first == "special-memory-usage")
+      continue; // Too expensive for get-all
+    std::string metricName = std::get<0>(e);
+
+    // Prometheus suggest using '_' instead of '-'
+    std::string prometheusMetricName = "dnsdist_" + boost::replace_all_copy(metricName, "-", "_");
+    if (metricBlacklist.count(metricName) != 0) {
+      continue;
+    }
 
-  using namespace json11;
-  vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
+    MetricDefinition metricDetails;
+    if (!s_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
+      vinfolog("Do not have metric details for %s", metricName);
+      continue;
+    }
 
-  try {
-    YaHTTP::AsyncRequestLoader yarl;
-    YaHTTP::Request req;
-    bool finished = false;
+    std::string prometheusTypeName = s_metricDefinitions.getPrometheusStringMetricType(metricDetails.prometheusType);
 
-    yarl.initialize(&req);
-    while(!finished) {
-      int bytes;
-      char buf[1024];
-      bytes = read(sock, buf, sizeof(buf));
-      if (bytes > 0) {
-        string data = string(buf, bytes);
-        finished = yarl.feed(data);
-      } else {
-        // read error OR EOF
-        break;
-      }
+    if (prometheusTypeName == "") {
+      vinfolog("Unknown Prometheus type for %s", metricName);
+      continue;
     }
-    yarl.finalize();
 
-    string command=req.getvars["command"];
+    // for these we have the help and types encoded in the sources:
+    output << "# HELP " << prometheusMetricName << " " << metricDetails.description    << "\n";
+    output << "# TYPE " << prometheusMetricName << " " << prometheusTypeName << "\n";
+    output << prometheusMetricName << " ";
 
-    req.getvars.erase("_"); // jQuery cache buster
+    if (const auto& val = boost::get<DNSDistStats::stat_t*>(&std::get<1>(e)))
+      output << (*val)->load();
+    else if (const auto& dval = boost::get<double*>(&std::get<1>(e)))
+      output << **dval;
+    else
+      output << (*boost::get<DNSDistStats::statfunction_t>(&std::get<1>(e)))(std::get<0>(e));
 
-    YaHTTP::Response resp;
-    resp.version = req.version;
-    const string charset = "; charset=utf-8";
+    output << "\n";
+  }
 
-    {
-      std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
+  // Latency histogram buckets
+  output << "# HELP dnsdist_latency Histogram of responses by latency (in milliseconds)\n";
+  output << "# TYPE dnsdist_latency histogram\n";
+  uint64_t latency_amounts = g_stats.latency0_1;
+  output << "dnsdist_latency_bucket{le=\"1\"} " << latency_amounts << "\n";
+  latency_amounts += g_stats.latency1_10;
+  output << "dnsdist_latency_bucket{le=\"10\"} " << latency_amounts << "\n";
+  latency_amounts += g_stats.latency10_50;
+  output << "dnsdist_latency_bucket{le=\"50\"} " << latency_amounts << "\n";
+  latency_amounts += g_stats.latency50_100;
+  output << "dnsdist_latency_bucket{le=\"100\"} " << latency_amounts << "\n";
+  latency_amounts += g_stats.latency100_1000;
+  output << "dnsdist_latency_bucket{le=\"1000\"} " << latency_amounts << "\n";
+  latency_amounts += g_stats.latencySlow; // Should be the same as latency_count
+  output << "dnsdist_latency_bucket{le=\"+Inf\"} " << latency_amounts << "\n";
+  output << "dnsdist_latency_sum " << g_stats.latencySum << "\n";
+  output << "dnsdist_latency_count " << getLatencyCount(std::string()) << "\n";
+
+  auto states = g_dstates.getLocal();
+  const string statesbase = "dnsdist_server_";
+
+  output << "# HELP " << statesbase << "status "                 << "Whether this backend is up (1) or down (0)"                        << "\n";
+  output << "# TYPE " << statesbase << "status "                 << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "queries "                << "Amount of queries relayed to server"                               << "\n";
+  output << "# TYPE " << statesbase << "queries "                << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "responses "              << "Amount of responses received from this server"                     << "\n";
+  output << "# TYPE " << statesbase << "responses "              << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "drops "                  << "Amount of queries not answered by server"                          << "\n";
+  output << "# TYPE " << statesbase << "drops "                  << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "latency "                << "Server's latency when answering questions in milliseconds"         << "\n";
+  output << "# TYPE " << statesbase << "latency "                << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "senderrors "             << "Total number of OS send errors while relaying queries"             << "\n";
+  output << "# TYPE " << statesbase << "senderrors "             << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "outstanding "            << "Current number of queries that are waiting for a backend response" << "\n";
+  output << "# TYPE " << statesbase << "outstanding "            << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "order "                  << "The order in which this server is picked"                          << "\n";
+  output << "# TYPE " << statesbase << "order "                  << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "weight "                 << "The weight within the order in which this server is picked"        << "\n";
+  output << "# TYPE " << statesbase << "weight "                 << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "tcpdiedsendingquery "    << "The number of TCP I/O errors while sending the query"              << "\n";
+  output << "# TYPE " << statesbase << "tcpdiedsendingquery "    << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "tcpdiedreadingresponse " << "The number of TCP I/O errors while reading the response"           << "\n";
+  output << "# TYPE " << statesbase << "tcpdiedreadingresponse " << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "tcpgaveup "              << "The number of TCP connections failing after too many attempts"     << "\n";
+  output << "# TYPE " << statesbase << "tcpgaveup "              << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "tcpreadtimeouts "        << "The number of TCP read timeouts"                                   << "\n";
+  output << "# TYPE " << statesbase << "tcpreadtimeouts "        << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "tcpwritetimeouts "       << "The number of TCP write timeouts"                                  << "\n";
+  output << "# TYPE " << statesbase << "tcpwritetimeouts "       << "counter"                                                           << "\n";
+  output << "# HELP " << statesbase << "tcpcurrentconnections "  << "The number of current TCP connections"                             << "\n";
+  output << "# TYPE " << statesbase << "tcpcurrentconnections "  << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "tcpavgqueriesperconn "   << "The average number of queries per TCP connection"                  << "\n";
+  output << "# TYPE " << statesbase << "tcpavgqueriesperconn "   << "gauge"                                                             << "\n";
+  output << "# HELP " << statesbase << "tcpavgconnduration "     << "The average duration of a TCP connection (ms)"                     << "\n";
+  output << "# TYPE " << statesbase << "tcpavgconnduration "     << "gauge"                                                             << "\n";
+
+  for (const auto& state : *states) {
+    string serverName;
+
+    if (state->getName().empty())
+      serverName = state->remote.toStringWithPort();
+    else
+      serverName = state->getName();
+
+    boost::replace_all(serverName, ".", "_");
+
+    const std::string label = boost::str(boost::format("{server=\"%1%\",address=\"%2%\"}")
+                                         % serverName % state->remote.toStringWithPort());
+
+    output << statesbase << "status"                 << label << " " << (state->isUp() ? "1" : "0")       << "\n";
+    output << statesbase << "queries"                << label << " " << state->queries.load()             << "\n";
+    output << statesbase << "responses"              << label << " " << state->responses.load()           << "\n";
+    output << statesbase << "drops"                  << label << " " << state->reuseds.load()             << "\n";
+    output << statesbase << "latency"                << label << " " << state->latencyUsec/1000.0         << "\n";
+    output << statesbase << "senderrors"             << label << " " << state->sendErrors.load()          << "\n";
+    output << statesbase << "outstanding"            << label << " " << state->outstanding.load()         << "\n";
+    output << statesbase << "order"                  << label << " " << state->order                      << "\n";
+    output << statesbase << "weight"                 << label << " " << state->weight                     << "\n";
+    output << statesbase << "tcpdiedsendingquery"    << label << " " << state->tcpDiedSendingQuery        << "\n";
+    output << statesbase << "tcpdiedreadingresponse" << label << " " << state->tcpDiedReadingResponse     << "\n";
+    output << statesbase << "tcpgaveup"              << label << " " << state->tcpGaveUp                  << "\n";
+    output << statesbase << "tcpreadtimeouts"        << label << " " << state->tcpReadTimeouts            << "\n";
+    output << statesbase << "tcpwritetimeouts"       << label << " " << state->tcpWriteTimeouts           << "\n";
+    output << statesbase << "tcpcurrentconnections"  << label << " " << state->tcpCurrentConnections      << "\n";
+    output << statesbase << "tcpavgqueriesperconn"   << label << " " << state->tcpAvgQueriesPerConnection << "\n";
+    output << statesbase << "tcpavgconnduration"     << label << " " << state->tcpAvgConnectionDuration   << "\n";
+  }
 
-      addCustomHeaders(resp, g_webserverConfig.customHeaders);
-      addSecurityHeaders(resp, g_webserverConfig.customHeaders);
+  const string frontsbase = "dnsdist_frontend_";
+  output << "# HELP " << frontsbase << "queries " << "Amount of queries received by this frontend" << "\n";
+  output << "# TYPE " << frontsbase << "queries " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "responses " << "Amount of responses sent by this frontend" << "\n";
+  output << "# TYPE " << frontsbase << "responses " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tcpdiedreadingquery " << "Amount of TCP connections terminated while reading the query from the client" << "\n";
+  output << "# TYPE " << frontsbase << "tcpdiedreadingquery " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tcpdiedsendingresponse " << "Amount of TCP connections terminated while sending a response to the client" << "\n";
+  output << "# TYPE " << frontsbase << "tcpdiedsendingresponse " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tcpgaveup " << "Amount of TCP connections terminated after too many attempts to get a connection to the backend" << "\n";
+  output << "# TYPE " << frontsbase << "tcpgaveup " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tcpclientimeouts " << "Amount of TCP connections terminated by a timeout while reading from the client" << "\n";
+  output << "# TYPE " << frontsbase << "tcpclientimeouts " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tcpdownstreamtimeouts " << "Amount of TCP connections terminated by a timeout while reading from the backend" << "\n";
+  output << "# TYPE " << frontsbase << "tcpdownstreamtimeouts " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tcpcurrentconnections " << "Amount of current incoming TCP connections from clients" << "\n";
+  output << "# TYPE " << frontsbase << "tcpcurrentconnections " << "gauge" << "\n";
+  output << "# HELP " << frontsbase << "tcpavgqueriesperconnection " << "The average number of queries per TCP connection" << "\n";
+  output << "# TYPE " << frontsbase << "tcpavgqueriesperconnection " << "gauge" << "\n";
+  output << "# HELP " << frontsbase << "tcpavgconnectionduration " << "The average duration of a TCP connection (ms)" << "\n";
+  output << "# TYPE " << frontsbase << "tcpavgconnectionduration " << "gauge" << "\n";
+  output << "# HELP " << frontsbase << "tlsqueries " << "Number of queries received by dnsdist over TLS, by TLS version" << "\n";
+  output << "# TYPE " << frontsbase << "tlsqueries " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tlsnewsessions " << "Amount of new TLS sessions negotiated" << "\n";
+  output << "# TYPE " << frontsbase << "tlsnewsessions " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tlsresumptions " << "Amount of TLS sessions resumed" << "\n";
+  output << "# TYPE " << frontsbase << "tlsresumptions " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tlsunknownticketkeys " << "Amount of attempts to resume TLS session from an unknown key (possibly expired)" << "\n";
+  output << "# TYPE " << frontsbase << "tlsunknownticketkeys " << "counter" << "\n";
+  output << "# HELP " << frontsbase << "tlsinactiveticketkeys " << "Amount of TLS sessions resumed from an inactive key" << "\n";
+  output << "# TYPE " << frontsbase << "tlsinactiveticketkeys " << "counter" << "\n";
+
+  output << "# HELP " << frontsbase << "tlshandshakefailures " << "Amount of TLS handshake failures" << "\n";
+  output << "# TYPE " << frontsbase << "tlshandshakefailures " << "counter" << "\n";
+
+  std::map<std::string,uint64_t> frontendDuplicates;
+  for (const auto& front : g_frontends) {
+    if (front->udpFD == -1 && front->tcpFD == -1)
+      continue;
+
+    const string frontName = front->local.toStringWithPort();
+    const string proto = front->getType();
+    const string fullName = frontName + "_" + proto;
+    uint64_t threadNumber = 0;
+    auto dupPair = frontendDuplicates.insert({fullName, 1});
+    if (!dupPair.second) {
+      threadNumber = dupPair.first->second;
+      ++(dupPair.first->second);
     }
-    /* indicate that the connection will be closed after completion of the response */
-    resp.headers["Connection"] = "close";
+    const std::string label = boost::str(boost::format("{frontend=\"%1%\",proto=\"%2%\",thread=\"%3%\"} ")
+                                         % frontName % proto % threadNumber);
+
+    output << frontsbase << "queries" << label << front->queries.load() << "\n";
+    output << frontsbase << "responses" << label << front->responses.load() << "\n";
+    if (front->isTCP()) {
+      output << frontsbase << "tcpdiedreadingquery" << label << front->tcpDiedReadingQuery.load() << "\n";
+      output << frontsbase << "tcpdiedsendingresponse" << label << front->tcpDiedSendingResponse.load() << "\n";
+      output << frontsbase << "tcpgaveup" << label << front->tcpGaveUp.load() << "\n";
+      output << frontsbase << "tcpclientimeouts" << label << front->tcpClientTimeouts.load() << "\n";
+      output << frontsbase << "tcpdownstreamtimeouts" << label << front->tcpDownstreamTimeouts.load() << "\n";
+      output << frontsbase << "tcpcurrentconnections" << label << front->tcpCurrentConnections.load() << "\n";
+      output << frontsbase << "tcpavgqueriesperconnection" << label << front->tcpAvgQueriesPerConnection.load() << "\n";
+      output << frontsbase << "tcpavgconnectionduration" << label << front->tcpAvgConnectionDuration.load() << "\n";
+      if (front->hasTLS()) {
+        output << frontsbase << "tlsnewsessions" << label << front->tlsNewSessions.load() << "\n";
+        output << frontsbase << "tlsresumptions" << label << front->tlsResumptions.load() << "\n";
+        output << frontsbase << "tlsunknownticketkeys" << label << front->tlsUnknownTicketKey.load() << "\n";
+        output << frontsbase << "tlsinactiveticketkeys" << label << front->tlsInactiveTicketKey.load() << "\n";
+
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls10\"} " << front->tls10queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls11\"} " << front->tls11queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls12\"} " << front->tls12queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls13\"} " << front->tls13queries.load() << "\n";
+        output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"unknown\"} " << front->tlsUnknownqueries.load() << "\n";
 
-    /* no need to send back the API key if any */
-    resp.headers.erase("X-API-Key");
+        const TLSErrorCounters* errorCounters = nullptr;
+        if (front->tlsFrontend != nullptr) {
+          errorCounters = &front->tlsFrontend->d_tlsCounters;
+        }
+        else if (front->dohFrontend != nullptr) {
+          errorCounters = &front->dohFrontend->d_tlsCounters;
+        }
 
-    if(req.method == "OPTIONS") {
-      /* the OPTIONS method should not require auth, otherwise it breaks CORS */
-      handleCORS(req, resp);
-      resp.status=200;
+        if (errorCounters != nullptr) {
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"dhKeyTooSmall\"} " << errorCounters->d_dhKeyTooSmall << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"inappropriateFallBack\"} " << errorCounters->d_inappropriateFallBack << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"noSharedCipher\"} " << errorCounters->d_noSharedCipher << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownCipherType\"} " << errorCounters->d_unknownCipherType << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownKeyExchangeType\"} " << errorCounters->d_unknownKeyExchangeType << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownProtocol\"} " << errorCounters->d_unknownProtocol << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedEC\"} " << errorCounters->d_unsupportedEC << "\n";
+          output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedProtocol{\"} " << errorCounters->d_unsupportedProtocol << "\n";
+        }
+      }
     }
-    else if (!compareAuthorization(req)) {
-      YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
-      if (header != req.headers.end())
-        errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, remote.toStringWithPort());
-      resp.status=401;
-      resp.body="<h1>Unauthorized</h1>";
-      resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
+  }
+
+  output << "# HELP " << frontsbase << "http_connects " << "Number of DoH TCP connections established to this frontend" << "\n";
+  output << "# TYPE " << frontsbase << "http_connects " << "counter" << "\n";
+
+  output << "# HELP " << frontsbase << "doh_http_method_queries " << "Number of DoH queries received by dnsdist, by HTTP method" << "\n";
+  output << "# TYPE " << frontsbase << "doh_http_method_queries " << "counter" << "\n";
+
+  output << "# HELP " << frontsbase << "doh_http_version_queries " << "Number of DoH queries received by dnsdist, by HTTP version" << "\n";
+  output << "# TYPE " << frontsbase << "doh_http_version_queries " << "counter" << "\n";
+
+  output << "# HELP " << frontsbase << "doh_bad_requests " << "Number of requests that could not be converted to a DNS query" << "\n";
+  output << "# TYPE " << frontsbase << "doh_bad_requests " << "counter" << "\n";
+
+  output << "# HELP " << frontsbase << "doh_responses " << "Number of responses sent, by type" << "\n";
+  output << "# TYPE " << frontsbase << "doh_responses " << "counter" << "\n";
 
+  output << "# HELP " << frontsbase << "doh_version_status_responses " << "Number of requests that could not be converted to a DNS query" << "\n";
+  output << "# TYPE " << frontsbase << "doh_version_status_responses " << "counter" << "\n";
+
+#ifdef HAVE_DNS_OVER_HTTPS
+  std::map<std::string,uint64_t> dohFrontendDuplicates;
+  for(const auto& doh : g_dohlocals) {
+    const string frontName = doh->d_local.toStringWithPort();
+    uint64_t threadNumber = 0;
+    auto dupPair = frontendDuplicates.insert({frontName, 1});
+    if (!dupPair.second) {
+      threadNumber = dupPair.first->second;
+      ++(dupPair.first->second);
     }
-    else if(!isMethodAllowed(req)) {
-      resp.status=405;
+    const std::string addrlabel = boost::str(boost::format("frontend=\"%1%\",thread=\"%2%\"") % frontName % threadNumber);
+    const std::string label = "{" + addrlabel + "} ";
+
+    output << frontsbase << "http_connects" << label << doh->d_httpconnects << "\n";
+    output << frontsbase << "doh_http_method_queries{method=\"get\"," << addrlabel << "} " << doh->d_getqueries << "\n";
+    output << frontsbase << "doh_http_method_queries{method=\"post\"," << addrlabel << "} " << doh->d_postqueries << "\n";
+
+    output << frontsbase << "doh_http_version_queries{version=\"1\"," << addrlabel << "} " << doh->d_http1Stats.d_nbQueries << "\n";
+    output << frontsbase << "doh_http_version_queries{version=\"2\"," << addrlabel << "} " << doh->d_http2Stats.d_nbQueries << "\n";
+
+    output << frontsbase << "doh_bad_requests{" << addrlabel << "} " << doh->d_badrequests << "\n";
+
+    output << frontsbase << "doh_responses{type=\"error\"," << addrlabel << "} " << doh->d_errorresponses << "\n";
+    output << frontsbase << "doh_responses{type=\"redirect\"," << addrlabel << "} " << doh->d_redirectresponses << "\n";
+    output << frontsbase << "doh_responses{type=\"valid\"," << addrlabel << "} " << doh->d_validresponses << "\n";
+
+    output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"200\"," << addrlabel << "} " << doh->d_http1Stats.d_nb200Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"400\"," << addrlabel << "} " << doh->d_http1Stats.d_nb400Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"403\"," << addrlabel << "} " << doh->d_http1Stats.d_nb403Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"500\"," << addrlabel << "} " << doh->d_http1Stats.d_nb500Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"502\"," << addrlabel << "} " << doh->d_http1Stats.d_nb502Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"other\"," << addrlabel << "} " << doh->d_http1Stats.d_nbOtherResponses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"200\"," << addrlabel << "} " << doh->d_http2Stats.d_nb200Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"400\"," << addrlabel << "} " << doh->d_http2Stats.d_nb400Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"403\"," << addrlabel << "} " << doh->d_http2Stats.d_nb403Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"500\"," << addrlabel << "} " << doh->d_http2Stats.d_nb500Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"502\"," << addrlabel << "} " << doh->d_http2Stats.d_nb502Responses << "\n";
+    output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"other\"," << addrlabel << "} " << doh->d_http2Stats.d_nbOtherResponses << "\n";
+  }
+#endif /* HAVE_DNS_OVER_HTTPS */
+
+  auto localPools = g_pools.getLocal();
+  const string cachebase = "dnsdist_pool_";
+  output << "# HELP dnsdist_pool_servers " << "Number of servers in that pool" << "\n";
+  output << "# TYPE dnsdist_pool_servers " << "gauge" << "\n";
+  output << "# HELP dnsdist_pool_active_servers " << "Number of available servers in that pool" << "\n";
+  output << "# TYPE dnsdist_pool_active_servers " << "gauge" << "\n";
+
+  output << "# HELP dnsdist_pool_cache_size " << "Maximum number of entries that this cache can hold" << "\n";
+  output << "# TYPE dnsdist_pool_cache_size " << "gauge" << "\n";
+  output << "# HELP dnsdist_pool_cache_entries " << "Number of entries currently present in that cache" << "\n";
+  output << "# TYPE dnsdist_pool_cache_entries " << "gauge" << "\n";
+  output << "# HELP dnsdist_pool_cache_hits " << "Number of hits from that cache" << "\n";
+  output << "# TYPE dnsdist_pool_cache_hits " << "counter" << "\n";
+  output << "# HELP dnsdist_pool_cache_misses " << "Number of misses from that cache" << "\n";
+  output << "# TYPE dnsdist_pool_cache_misses " << "counter" << "\n";
+  output << "# HELP dnsdist_pool_cache_deferred_inserts " << "Number of insertions into that cache skipped because it was already locked" << "\n";
+  output << "# TYPE dnsdist_pool_cache_deferred_inserts " << "counter" << "\n";
+  output << "# HELP dnsdist_pool_cache_deferred_lookups " << "Number of lookups into that cache skipped because it was already locked" << "\n";
+  output << "# TYPE dnsdist_pool_cache_deferred_lookups " << "counter" << "\n";
+  output << "# HELP dnsdist_pool_cache_lookup_collisions " << "Number of lookups into that cache that triggered a collision (same hash but different entry)" << "\n";
+  output << "# TYPE dnsdist_pool_cache_lookup_collisions " << "counter" << "\n";
+  output << "# HELP dnsdist_pool_cache_insert_collisions " << "Number of insertions into that cache that triggered a collision (same hash but different entry)" << "\n";
+  output << "# TYPE dnsdist_pool_cache_insert_collisions " << "counter" << "\n";
+  output << "# HELP dnsdist_pool_cache_ttl_too_shorts " << "Number of insertions into that cache skipped because the TTL of the answer was not long enough" << "\n";
+  output << "# TYPE dnsdist_pool_cache_ttl_too_shorts " << "counter" << "\n";
+
+  for (const auto& entry : *localPools) {
+    string poolName = entry.first;
+
+    if (poolName.empty()) {
+      poolName = "_default_";
     }
-    else if(req.url.path=="/jsonstat") {
-      handleCORS(req, resp);
-      resp.status=200;
-
-      if(command=="stats") {
-        auto obj=Json::object {
-          { "packetcache-hits", 0},
-          { "packetcache-misses", 0},
-          { "over-capacity-drops", 0 },
-          { "too-old-drops", 0 },
-          { "server-policy", g_policy.getLocal()->getName()}
-        };
+    const string label = "{pool=\"" + poolName + "\"}";
+    const std::shared_ptr<ServerPool> pool = entry.second;
+    output << "dnsdist_pool_servers" << label << " " << pool->countServers(false) << "\n";
+    output << "dnsdist_pool_active_servers" << label << " " << pool->countServers(true) << "\n";
+
+    if (pool->packetCache != nullptr) {
+      const auto& cache = pool->packetCache;
+
+      output << cachebase << "cache_size"              <<label << " " << cache->getMaxEntries()       << "\n";
+      output << cachebase << "cache_entries"           <<label << " " << cache->getEntriesCount()     << "\n";
+      output << cachebase << "cache_hits"              <<label << " " << cache->getHits()             << "\n";
+      output << cachebase << "cache_misses"            <<label << " " << cache->getMisses()           << "\n";
+      output << cachebase << "cache_deferred_inserts"  <<label << " " << cache->getDeferredInserts()  << "\n";
+      output << cachebase << "cache_deferred_lookups"  <<label << " " << cache->getDeferredLookups()  << "\n";
+      output << cachebase << "cache_lookup_collisions" <<label << " " << cache->getLookupCollisions() << "\n";
+      output << cachebase << "cache_insert_collisions" <<label << " " << cache->getInsertCollisions() << "\n";
+      output << cachebase << "cache_ttl_too_shorts"    <<label << " " << cache->getTTLTooShorts()     << "\n";
+    }
+  }
 
-        for(const auto& e : g_stats.entries) {
-          if (e.first == "special-memory-usage")
-            continue; // Too expensive for get-all
-          if(const auto& val = boost::get<DNSDistStats::stat_t*>(&e.second))
-            obj.insert({e.first, (double)(*val)->load()});
-          else if (const auto& dval = boost::get<double*>(&e.second))
-            obj.insert({e.first, (**dval)});
-          else
-            obj.insert({e.first, (double)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
-        }
-        Json my_json = obj;
-        resp.body=my_json.dump();
-        resp.headers["Content-Type"] = "application/json";
-      }
-      else if(command=="dynblocklist") {
-        Json::object obj;
-        auto nmg = g_dynblockNMG.getLocal();
-        struct timespec now;
-        gettime(&now);
-        for(const auto& e: *nmg) {
-          if(now < e.second.until ) {
-            Json::object thing{
-              {"reason", e.second.reason},
-              {"seconds", (double)(e.second.until.tv_sec - now.tv_sec)},
-              {"blocks", (double)e.second.blocks},
-              {"action", DNSAction::typeToString(e.second.action != DNSAction::Action::None ? e.second.action : g_dynBlockAction) },
-              {"warning", e.second.warning }
-            };
-            obj.insert({e.first.toString(), thing});
-          }
-        }
+  output << "# HELP dnsdist_info " << "Info from dnsdist, value is always 1" << "\n";
+  output << "# TYPE dnsdist_info " << "gauge" << "\n";
+  output << "dnsdist_info{version=\"" << VERSION << "\"} " << "1" << "\n";
 
-        auto smt = g_dynblockSMT.getLocal();
-        smt->visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
-            if(now <node.d_value.until) {
-              string dom("empty");
-              if(!node.d_value.domain.empty())
-                dom = node.d_value.domain.toString();
-              Json::object thing{
-                {"reason", node.d_value.reason},
-                {"seconds", (double)(node.d_value.until.tv_sec - now.tv_sec)},
-                {"blocks", (double)node.d_value.blocks},
-                {"action", DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction) }
-              };
-              obj.insert({dom, thing});
-          }
-        });
+  resp.body = output.str();
+  resp.headers["Content-Type"] = "text/plain";
+}
+
+static void handleJSONStats(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  handleCORS(req, resp);
+  resp.status = 200;
 
+  if (req.getvars.count("command") == 0) {
+    resp.status = 404;
+    return;
+  }
 
+  const string& command = req.getvars.at("command");
 
-        Json my_json = obj;
-        resp.body=my_json.dump();
-        resp.headers["Content-Type"] = "application/json";
+  if (command == "stats") {
+    auto obj=Json::object {
+      { "packetcache-hits", 0},
+      { "packetcache-misses", 0},
+      { "over-capacity-drops", 0 },
+      { "too-old-drops", 0 },
+      { "server-policy", g_policy.getLocal()->getName()}
+    };
+
+    for (const auto& e : g_stats.entries) {
+      if (e.first == "special-memory-usage")
+        continue; // Too expensive for get-all
+      if(const auto& val = boost::get<DNSDistStats::stat_t*>(&e.second))
+        obj.insert({e.first, (double)(*val)->load()});
+      else if (const auto& dval = boost::get<double*>(&e.second))
+        obj.insert({e.first, (**dval)});
+      else
+        obj.insert({e.first, (double)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
+    }
+    Json my_json = obj;
+    resp.body = my_json.dump();
+    resp.headers["Content-Type"] = "application/json";
+  }
+  else if (command == "dynblocklist") {
+    Json::object obj;
+    auto nmg = g_dynblockNMG.getLocal();
+    struct timespec now;
+    gettime(&now);
+    for (const auto& e: *nmg) {
+      if(now < e.second.until ) {
+        Json::object thing{
+          {"reason", e.second.reason},
+          {"seconds", (double)(e.second.until.tv_sec - now.tv_sec)},
+          {"blocks", (double)e.second.blocks},
+          {"action", DNSAction::typeToString(e.second.action != DNSAction::Action::None ? e.second.action : g_dynBlockAction) },
+          {"warning", e.second.warning }
+        };
+        obj.insert({e.first.toString(), thing});
       }
-      else if(command=="ebpfblocklist") {
-        Json::object obj;
-#ifdef HAVE_EBPF
-        struct timespec now;
-        gettime(&now);
-        for (const auto& dynbpf : g_dynBPFFilters) {
-          std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > addrStats = dynbpf->getAddrStats();
-          for (const auto& entry : addrStats) {
-            Json::object thing
-            {
-              {"seconds", (double)(std::get<2>(entry).tv_sec - now.tv_sec)},
-              {"blocks", (double)(std::get<1>(entry))}
-            };
-            obj.insert({std::get<0>(entry).toString(), thing });
-          }
-        }
-#endif /* HAVE_EBPF */
-        Json my_json = obj;
-        resp.body=my_json.dump();
-        resp.headers["Content-Type"] = "application/json";
+    }
+
+    auto smt = g_dynblockSMT.getLocal();
+    smt->visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
+      if(now <node.d_value.until) {
+        string dom("empty");
+        if(!node.d_value.domain.empty())
+          dom = node.d_value.domain.toString();
+        Json::object thing{
+          {"reason", node.d_value.reason},
+          {"seconds", (double)(node.d_value.until.tv_sec - now.tv_sec)},
+          {"blocks", (double)node.d_value.blocks},
+          {"action", DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction) }
+        };
+        obj.insert({dom, thing});
       }
-      else {
-        resp.status=404;
+    });
+
+    Json my_json = obj;
+    resp.body = my_json.dump();
+    resp.headers["Content-Type"] = "application/json";
+  }
+  else if (command == "ebpfblocklist") {
+    Json::object obj;
+#ifdef HAVE_EBPF
+    struct timespec now;
+    gettime(&now);
+    for (const auto& dynbpf : g_dynBPFFilters) {
+      std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > addrStats = dynbpf->getAddrStats();
+      for (const auto& entry : addrStats) {
+        Json::object thing
+          {
+            {"seconds", (double)(std::get<2>(entry).tv_sec - now.tv_sec)},
+            {"blocks", (double)(std::get<1>(entry))}
+          };
+        obj.insert({std::get<0>(entry).toString(), thing });
       }
     }
-    else if (req.url.path == "/metrics") {
-        handleCORS(req, resp);
-        resp.status = 200;
-
-        std::ostringstream output;
-        static const std::set<std::string> metricBlacklist = { "latency-count", "latency-sum" };
-        for (const auto& e : g_stats.entries) {
-          if (e.first == "special-memory-usage")
-            continue; // Too expensive for get-all
-          std::string metricName = std::get<0>(e);
-
-          // Prometheus suggest using '_' instead of '-'
-          std::string prometheusMetricName = "dnsdist_" + boost::replace_all_copy(metricName, "-", "_");
-          if (metricBlacklist.count(metricName) != 0) {
-            continue;
-          }
+#endif /* HAVE_EBPF */
+    Json my_json = obj;
+    resp.body = my_json.dump();
+    resp.headers["Content-Type"] = "application/json";
+  }
+  else {
+    resp.status = 404;
+  }
+}
 
-          MetricDefinition metricDetails;
-          if (!s_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
-              vinfolog("Do not have metric details for %s", metricName);
-              continue;
-          }
+static void handleStats(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  handleCORS(req, resp);
+  resp.status = 200;
+
+  Json::array servers;
+  auto localServers = g_dstates.getLocal();
+  int num = 0;
+  for (const auto& a : *localServers) {
+    string status;
+    if(a->availability == DownstreamState::Availability::Up)
+      status = "UP";
+    else if(a->availability == DownstreamState::Availability::Down)
+      status = "DOWN";
+    else
+      status = (a->upStatus ? "up" : "down");
+
+    Json::array pools;
+    for(const auto& p: a->pools)
+      pools.push_back(p);
+
+    Json::object server{
+      {"id", num++},
+      {"name", a->getName()},
+      {"address", a->remote.toStringWithPort()},
+      {"state", status},
+      {"qps", (double)a->queryLoad},
+      {"qpsLimit", (double)a->qps.getRate()},
+      {"outstanding", (double)a->outstanding},
+      {"reuseds", (double)a->reuseds},
+      {"weight", (double)a->weight},
+      {"order", (double)a->order},
+      {"pools", pools},
+      {"latency", (double)(a->latencyUsec/1000.0)},
+      {"queries", (double)a->queries},
+      {"responses", (double)a->responses},
+      {"sendErrors", (double)a->sendErrors},
+      {"tcpDiedSendingQuery", (double)a->tcpDiedSendingQuery},
+      {"tcpDiedReadingResponse", (double)a->tcpDiedReadingResponse},
+      {"tcpGaveUp", (double)a->tcpGaveUp},
+      {"tcpReadTimeouts", (double)a->tcpReadTimeouts},
+      {"tcpWriteTimeouts", (double)a->tcpWriteTimeouts},
+      {"tcpCurrentConnections", (double)a->tcpCurrentConnections},
+      {"tcpAvgQueriesPerConnection", (double)a->tcpAvgQueriesPerConnection},
+      {"tcpAvgConnectionDuration", (double)a->tcpAvgConnectionDuration},
+      {"dropRate", (double)a->dropRate}
+    };
 
-          std::string prometheusTypeName = s_metricDefinitions.getPrometheusStringMetricType(metricDetails.prometheusType);
+    /* sending a latency for a DOWN server doesn't make sense */
+    if (a->availability == DownstreamState::Availability::Down) {
+      server["latency"] = nullptr;
+    }
 
-          if (prometheusTypeName == "") {
-              vinfolog("Unknown Prometheus type for %s", metricName);
-              continue;
-          }
+    servers.push_back(server);
+  }
 
-          // for these we have the help and types encoded in the sources:
-          output << "# HELP " << prometheusMetricName << " " << metricDetails.description    << "\n";
-          output << "# TYPE " << prometheusMetricName << " " << prometheusTypeName << "\n";
-          output << prometheusMetricName << " ";
+  Json::array frontends;
+  num = 0;
+  for(const auto& front : g_frontends) {
+    if (front->udpFD == -1 && front->tcpFD == -1)
+      continue;
+    Json::object frontend{
+      { "id", num++ },
+      { "address", front->local.toStringWithPort() },
+      { "udp", front->udpFD >= 0 },
+      { "tcp", front->tcpFD >= 0 },
+      { "type", front->getType() },
+      { "queries", (double) front->queries.load() },
+      { "responses", (double) front->responses.load() },
+      { "tcpDiedReadingQuery", (double) front->tcpDiedReadingQuery.load() },
+      { "tcpDiedSendingResponse", (double) front->tcpDiedSendingResponse.load() },
+      { "tcpGaveUp", (double) front->tcpGaveUp.load() },
+      { "tcpClientTimeouts", (double) front->tcpClientTimeouts },
+      { "tcpDownstreamTimeouts", (double) front->tcpDownstreamTimeouts },
+      { "tcpCurrentConnections", (double) front->tcpCurrentConnections },
+      { "tcpAvgQueriesPerConnection", (double) front->tcpAvgQueriesPerConnection },
+      { "tcpAvgConnectionDuration", (double) front->tcpAvgConnectionDuration },
+      { "tlsNewSessions", (double) front->tlsNewSessions },
+      { "tlsResumptions", (double) front->tlsResumptions },
+      { "tlsUnknownTicketKey", (double) front->tlsUnknownTicketKey },
+      { "tlsInactiveTicketKey", (double) front->tlsInactiveTicketKey },
+      { "tls10Queries", (double) front->tls10queries },
+      { "tls11Queries", (double) front->tls11queries },
+      { "tls12Queries", (double) front->tls12queries },
+      { "tls13Queries", (double) front->tls13queries },
+      { "tlsUnknownQueries", (double) front->tlsUnknownqueries },
+    };
+    const TLSErrorCounters* errorCounters = nullptr;
+    if (front->tlsFrontend != nullptr) {
+      errorCounters = &front->tlsFrontend->d_tlsCounters;
+    }
+    else if (front->dohFrontend != nullptr) {
+      errorCounters = &front->dohFrontend->d_tlsCounters;
+    }
+    if (errorCounters != nullptr) {
+      frontend["tlsHandshakeFailuresDHKeyTooSmall"] = (double)errorCounters->d_dhKeyTooSmall;
+      frontend["tlsHandshakeFailuresInappropriateFallBack"] = (double)errorCounters->d_inappropriateFallBack;
+      frontend["tlsHandshakeFailuresNoSharedCipher"] = (double)errorCounters->d_noSharedCipher;
+      frontend["tlsHandshakeFailuresUnknownCipher"] = (double)errorCounters->d_unknownCipherType;
+      frontend["tlsHandshakeFailuresUnknownKeyExchangeType"] = (double)errorCounters->d_unknownKeyExchangeType;
+      frontend["tlsHandshakeFailuresUnknownProtocol"] = (double)errorCounters->d_unknownProtocol;
+      frontend["tlsHandshakeFailuresUnsupportedEC"] = (double)errorCounters->d_unsupportedEC;
+      frontend["tlsHandshakeFailuresUnsupportedProtocol"] = (double)errorCounters->d_unsupportedProtocol;
+    }
+    frontends.push_back(frontend);
+  }
 
-          if (const auto& val = boost::get<DNSDistStats::stat_t*>(&std::get<1>(e)))
-            output << (*val)->load();
-          else if (const auto& dval = boost::get<double*>(&std::get<1>(e)))
-            output << **dval;
-          else
-            output << (*boost::get<DNSDistStats::statfunction_t>(&std::get<1>(e)))(std::get<0>(e));
+  Json::array dohs;
+#ifdef HAVE_DNS_OVER_HTTPS
+  {
+    num = 0;
+    for(const auto& doh : g_dohlocals) {
+      Json::object obj{
+        { "id", num++ },
+        { "address", doh->d_local.toStringWithPort() },
+        { "http-connects", (double) doh->d_httpconnects },
+        { "http1-queries", (double) doh->d_http1Stats.d_nbQueries },
+        { "http2-queries", (double) doh->d_http2Stats.d_nbQueries },
+        { "http1-200-responses", (double) doh->d_http1Stats.d_nb200Responses },
+        { "http2-200-responses", (double) doh->d_http2Stats.d_nb200Responses },
+        { "http1-400-responses", (double) doh->d_http1Stats.d_nb400Responses },
+        { "http2-400-responses", (double) doh->d_http2Stats.d_nb400Responses },
+        { "http1-403-responses", (double) doh->d_http1Stats.d_nb403Responses },
+        { "http2-403-responses", (double) doh->d_http2Stats.d_nb403Responses },
+        { "http1-500-responses", (double) doh->d_http1Stats.d_nb500Responses },
+        { "http2-500-responses", (double) doh->d_http2Stats.d_nb500Responses },
+        { "http1-502-responses", (double) doh->d_http1Stats.d_nb502Responses },
+        { "http2-502-responses", (double) doh->d_http2Stats.d_nb502Responses },
+        { "http1-other-responses", (double) doh->d_http1Stats.d_nbOtherResponses },
+        { "http2-other-responses", (double) doh->d_http2Stats.d_nbOtherResponses },
+        { "get-queries", (double) doh->d_getqueries },
+        { "post-queries", (double) doh->d_postqueries },
+        { "bad-requests", (double) doh->d_badrequests },
+        { "error-responses", (double) doh->d_errorresponses },
+        { "redirect-responses", (double) doh->d_redirectresponses },
+        { "valid-responses", (double) doh->d_validresponses }
+      };
+      dohs.push_back(obj);
+    }
+  }
+#endif /* HAVE_DNS_OVER_HTTPS */
 
-          output << "\n";
-        }
+  Json::array pools;
+  auto localPools = g_pools.getLocal();
+  num = 0;
+  for(const auto& pool : *localPools) {
+    const auto& cache = pool.second->packetCache;
+    Json::object entry {
+      { "id", num++ },
+      { "name", pool.first },
+      { "serversCount", (double) pool.second->countServers(false) },
+      { "cacheSize", (double) (cache ? cache->getMaxEntries() : 0) },
+      { "cacheEntries", (double) (cache ? cache->getEntriesCount() : 0) },
+      { "cacheHits", (double) (cache ? cache->getHits() : 0) },
+      { "cacheMisses", (double) (cache ? cache->getMisses() : 0) },
+      { "cacheDeferredInserts", (double) (cache ? cache->getDeferredInserts() : 0) },
+      { "cacheDeferredLookups", (double) (cache ? cache->getDeferredLookups() : 0) },
+      { "cacheLookupCollisions", (double) (cache ? cache->getLookupCollisions() : 0) },
+      { "cacheInsertCollisions", (double) (cache ? cache->getInsertCollisions() : 0) },
+      { "cacheTTLTooShorts", (double) (cache ? cache->getTTLTooShorts() : 0) }
+    };
+    pools.push_back(entry);
+  }
 
-        // Latency histogram buckets
-        output << "# HELP dnsdist_latency Histogram of responses by latency (in milliseconds)\n";
-        output << "# TYPE dnsdist_latency histogram\n";
-        uint64_t latency_amounts = g_stats.latency0_1;
-        output << "dnsdist_latency_bucket{le=\"1\"} " << latency_amounts << "\n";
-        latency_amounts += g_stats.latency1_10;
-        output << "dnsdist_latency_bucket{le=\"10\"} " << latency_amounts << "\n";
-        latency_amounts += g_stats.latency10_50;
-        output << "dnsdist_latency_bucket{le=\"50\"} " << latency_amounts << "\n";
-        latency_amounts += g_stats.latency50_100;
-        output << "dnsdist_latency_bucket{le=\"100\"} " << latency_amounts << "\n";
-        latency_amounts += g_stats.latency100_1000;
-        output << "dnsdist_latency_bucket{le=\"1000\"} " << latency_amounts << "\n";
-        latency_amounts += g_stats.latencySlow; // Should be the same as latency_count
-        output << "dnsdist_latency_bucket{le=\"+Inf\"} " << latency_amounts << "\n";
-        output << "dnsdist_latency_sum " << g_stats.latencySum << "\n";
-        output << "dnsdist_latency_count " << getLatencyCount(std::string()) << "\n";
-
-        auto states = g_dstates.getLocal();
-        const string statesbase = "dnsdist_server_";
-
-        output << "# HELP " << statesbase << "status "                 << "Whether this backend is up (1) or down (0)"                        << "\n";
-        output << "# TYPE " << statesbase << "status "                 << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "queries "                << "Amount of queries relayed to server"                               << "\n";
-        output << "# TYPE " << statesbase << "queries "                << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "responses "              << "Amount of responses received from this server"                     << "\n";
-        output << "# TYPE " << statesbase << "responses "              << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "drops "                  << "Amount of queries not answered by server"                          << "\n";
-        output << "# TYPE " << statesbase << "drops "                  << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "latency "                << "Server's latency when answering questions in milliseconds"         << "\n";
-        output << "# TYPE " << statesbase << "latency "                << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "senderrors "             << "Total number of OS send errors while relaying queries"             << "\n";
-        output << "# TYPE " << statesbase << "senderrors "             << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "outstanding "            << "Current number of queries that are waiting for a backend response" << "\n";
-        output << "# TYPE " << statesbase << "outstanding "            << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "order "                  << "The order in which this server is picked"                          << "\n";
-        output << "# TYPE " << statesbase << "order "                  << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "weight "                 << "The weight within the order in which this server is picked"        << "\n";
-        output << "# TYPE " << statesbase << "weight "                 << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "tcpdiedsendingquery "    << "The number of TCP I/O errors while sending the query"              << "\n";
-        output << "# TYPE " << statesbase << "tcpdiedsendingquery "    << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "tcpdiedreadingresponse " << "The number of TCP I/O errors while reading the response"           << "\n";
-        output << "# TYPE " << statesbase << "tcpdiedreadingresponse " << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "tcpgaveup "              << "The number of TCP connections failing after too many attempts"     << "\n";
-        output << "# TYPE " << statesbase << "tcpgaveup "              << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "tcpreadtimeouts "        << "The number of TCP read timeouts"                                   << "\n";
-        output << "# TYPE " << statesbase << "tcpreadtimeouts "        << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "tcpwritetimeouts "       << "The number of TCP write timeouts"                                  << "\n";
-        output << "# TYPE " << statesbase << "tcpwritetimeouts "       << "counter"                                                           << "\n";
-        output << "# HELP " << statesbase << "tcpcurrentconnections "  << "The number of current TCP connections"                             << "\n";
-        output << "# TYPE " << statesbase << "tcpcurrentconnections "  << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "tcpavgqueriesperconn "   << "The average number of queries per TCP connection"                  << "\n";
-        output << "# TYPE " << statesbase << "tcpavgqueriesperconn "   << "gauge"                                                             << "\n";
-        output << "# HELP " << statesbase << "tcpavgconnduration "     << "The average duration of a TCP connection (ms)"                     << "\n";
-        output << "# TYPE " << statesbase << "tcpavgconnduration "     << "gauge"                                                             << "\n";
-
-        for (const auto& state : *states) {
-          string serverName;
-
-          if (state->getName().empty())
-              serverName = state->remote.toStringWithPort();
-          else
-              serverName = state->getName();
-
-          boost::replace_all(serverName, ".", "_");
-
-          const std::string label = boost::str(boost::format("{server=\"%1%\",address=\"%2%\"}")
-            % serverName % state->remote.toStringWithPort());
-
-          output << statesbase << "status"                 << label << " " << (state->isUp() ? "1" : "0")       << "\n";
-          output << statesbase << "queries"                << label << " " << state->queries.load()             << "\n";
-          output << statesbase << "responses"              << label << " " << state->responses.load()           << "\n";
-          output << statesbase << "drops"                  << label << " " << state->reuseds.load()             << "\n";
-          output << statesbase << "latency"                << label << " " << state->latencyUsec/1000.0         << "\n";
-          output << statesbase << "senderrors"             << label << " " << state->sendErrors.load()          << "\n";
-          output << statesbase << "outstanding"            << label << " " << state->outstanding.load()         << "\n";
-          output << statesbase << "order"                  << label << " " << state->order                      << "\n";
-          output << statesbase << "weight"                 << label << " " << state->weight                     << "\n";
-          output << statesbase << "tcpdiedsendingquery"    << label << " " << state->tcpDiedSendingQuery        << "\n";
-          output << statesbase << "tcpdiedreadingresponse" << label << " " << state->tcpDiedReadingResponse     << "\n";
-          output << statesbase << "tcpgaveup"              << label << " " << state->tcpGaveUp                  << "\n";
-          output << statesbase << "tcpreadtimeouts"        << label << " " << state->tcpReadTimeouts            << "\n";
-          output << statesbase << "tcpwritetimeouts"       << label << " " << state->tcpWriteTimeouts           << "\n";
-          output << statesbase << "tcpcurrentconnections"  << label << " " << state->tcpCurrentConnections      << "\n";
-          output << statesbase << "tcpavgqueriesperconn"   << label << " " << state->tcpAvgQueriesPerConnection << "\n";
-          output << statesbase << "tcpavgconnduration"     << label << " " << state->tcpAvgConnectionDuration   << "\n";
-        }
+  Json::array rules;
+  auto localRules = g_rulactions.getLocal();
+  num=0;
+  for(const auto& a : *localRules) {
+    Json::object rule{
+      {"id", num++},
+      {"creationOrder", (double)a.d_creationOrder},
+      {"uuid", boost::uuids::to_string(a.d_id)},
+      {"matches", (double)a.d_rule->d_matches},
+      {"rule", a.d_rule->toString()},
+      {"action", a.d_action->toString()},
+      {"action-stats", a.d_action->getStats()}
+    };
+    rules.push_back(rule);
+  }
 
-        const string frontsbase = "dnsdist_frontend_";
-        output << "# HELP " << frontsbase << "queries " << "Amount of queries received by this frontend" << "\n";
-        output << "# TYPE " << frontsbase << "queries " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "responses " << "Amount of responses sent by this frontend" << "\n";
-        output << "# TYPE " << frontsbase << "responses " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tcpdiedreadingquery " << "Amount of TCP connections terminated while reading the query from the client" << "\n";
-        output << "# TYPE " << frontsbase << "tcpdiedreadingquery " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tcpdiedsendingresponse " << "Amount of TCP connections terminated while sending a response to the client" << "\n";
-        output << "# TYPE " << frontsbase << "tcpdiedsendingresponse " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tcpgaveup " << "Amount of TCP connections terminated after too many attempts to get a connection to the backend" << "\n";
-        output << "# TYPE " << frontsbase << "tcpgaveup " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tcpclientimeouts " << "Amount of TCP connections terminated by a timeout while reading from the client" << "\n";
-        output << "# TYPE " << frontsbase << "tcpclientimeouts " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tcpdownstreamtimeouts " << "Amount of TCP connections terminated by a timeout while reading from the backend" << "\n";
-        output << "# TYPE " << frontsbase << "tcpdownstreamtimeouts " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tcpcurrentconnections " << "Amount of current incoming TCP connections from clients" << "\n";
-        output << "# TYPE " << frontsbase << "tcpcurrentconnections " << "gauge" << "\n";
-        output << "# HELP " << frontsbase << "tcpavgqueriesperconnection " << "The average number of queries per TCP connection" << "\n";
-        output << "# TYPE " << frontsbase << "tcpavgqueriesperconnection " << "gauge" << "\n";
-        output << "# HELP " << frontsbase << "tcpavgconnectionduration " << "The average duration of a TCP connection (ms)" << "\n";
-        output << "# TYPE " << frontsbase << "tcpavgconnectionduration " << "gauge" << "\n";
-        output << "# HELP " << frontsbase << "tlsqueries " << "Number of queries received by dnsdist over TLS, by TLS version" << "\n";
-        output << "# TYPE " << frontsbase << "tlsqueries " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tlsnewsessions " << "Amount of new TLS sessions negotiated" << "\n";
-        output << "# TYPE " << frontsbase << "tlsnewsessions " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tlsresumptions " << "Amount of TLS sessions resumed" << "\n";
-        output << "# TYPE " << frontsbase << "tlsresumptions " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tlsunknownticketkeys " << "Amount of attempts to resume TLS session from an unknown key (possibly expired)" << "\n";
-        output << "# TYPE " << frontsbase << "tlsunknownticketkeys " << "counter" << "\n";
-        output << "# HELP " << frontsbase << "tlsinactiveticketkeys " << "Amount of TLS sessions resumed from an inactive key" << "\n";
-        output << "# TYPE " << frontsbase << "tlsinactiveticketkeys " << "counter" << "\n";
-
-        output << "# HELP " << frontsbase << "tlshandshakefailures " << "Amount of TLS handshake failures" << "\n";
-        output << "# TYPE " << frontsbase << "tlshandshakefailures " << "counter" << "\n";
-
-        std::map<std::string,uint64_t> frontendDuplicates;
-        for (const auto& front : g_frontends) {
-          if (front->udpFD == -1 && front->tcpFD == -1)
-            continue;
-
-          const string frontName = front->local.toStringWithPort();
-          const string proto = front->getType();
-          const string fullName = frontName + "_" + proto;
-          uint64_t threadNumber = 0;
-          auto dupPair = frontendDuplicates.insert({fullName, 1});
-          if (!dupPair.second) {
-            threadNumber = dupPair.first->second;
-            ++(dupPair.first->second);
-          }
-          const std::string label = boost::str(boost::format("{frontend=\"%1%\",proto=\"%2%\",thread=\"%3%\"} ")
-            % frontName % proto % threadNumber);
-
-          output << frontsbase << "queries" << label << front->queries.load() << "\n";
-          output << frontsbase << "responses" << label << front->responses.load() << "\n";
-          if (front->isTCP()) {
-            output << frontsbase << "tcpdiedreadingquery" << label << front->tcpDiedReadingQuery.load() << "\n";
-            output << frontsbase << "tcpdiedsendingresponse" << label << front->tcpDiedSendingResponse.load() << "\n";
-            output << frontsbase << "tcpgaveup" << label << front->tcpGaveUp.load() << "\n";
-            output << frontsbase << "tcpclientimeouts" << label << front->tcpClientTimeouts.load() << "\n";
-            output << frontsbase << "tcpdownstreamtimeouts" << label << front->tcpDownstreamTimeouts.load() << "\n";
-            output << frontsbase << "tcpcurrentconnections" << label << front->tcpCurrentConnections.load() << "\n";
-            output << frontsbase << "tcpavgqueriesperconnection" << label << front->tcpAvgQueriesPerConnection.load() << "\n";
-            output << frontsbase << "tcpavgconnectionduration" << label << front->tcpAvgConnectionDuration.load() << "\n";
-            if (front->hasTLS()) {
-              output << frontsbase << "tlsnewsessions" << label << front->tlsNewSessions.load() << "\n";
-              output << frontsbase << "tlsresumptions" << label << front->tlsResumptions.load() << "\n";
-              output << frontsbase << "tlsunknownticketkeys" << label << front->tlsUnknownTicketKey.load() << "\n";
-              output << frontsbase << "tlsinactiveticketkeys" << label << front->tlsInactiveTicketKey.load() << "\n";
-
-              output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls10\"} " << front->tls10queries.load() << "\n";
-              output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls11\"} " << front->tls11queries.load() << "\n";
-              output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls12\"} " << front->tls12queries.load() << "\n";
-              output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls13\"} " << front->tls13queries.load() << "\n";
-              output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"unknown\"} " << front->tlsUnknownqueries.load() << "\n";
-
-              const TLSErrorCounters* errorCounters = nullptr;
-              if (front->tlsFrontend != nullptr) {
-                errorCounters = &front->tlsFrontend->d_tlsCounters;
-              }
-              else if (front->dohFrontend != nullptr) {
-                errorCounters = &front->dohFrontend->d_tlsCounters;
-              }
-
-              if (errorCounters != nullptr) {
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"dhKeyTooSmall\"} " << errorCounters->d_dhKeyTooSmall << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"inappropriateFallBack\"} " << errorCounters->d_inappropriateFallBack << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"noSharedCipher\"} " << errorCounters->d_noSharedCipher << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownCipherType\"} " << errorCounters->d_unknownCipherType << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownKeyExchangeType\"} " << errorCounters->d_unknownKeyExchangeType << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownProtocol\"} " << errorCounters->d_unknownProtocol << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedEC\"} " << errorCounters->d_unsupportedEC << "\n";
-                output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedProtocol{\"} " << errorCounters->d_unsupportedProtocol << "\n";
-              }
-            }
-          }
-        }
+  auto responseRules = someResponseRulesToJson(&g_resprulactions);
+  auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitresprulactions);
+  auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredresprulactions);
+
+  string acl;
+
+  vector<string> vec;
+  g_ACL.getLocal()->toStringVector(&vec);
+
+  for(const auto& s : vec) {
+    if(!acl.empty()) acl += ", ";
+    acl+=s;
+  }
+  string localaddressesStr;
+  std::set<std::string> localaddresses;
+  for(const auto& front : g_frontends) {
+    localaddresses.insert(front->local.toStringWithPort());
+  }
+  for (const auto& addr : localaddresses) {
+    if (!localaddressesStr.empty()) {
+      localaddressesStr += ", ";
+    }
+    localaddressesStr += addr;
+  }
+
+  Json my_json = Json::object {
+    { "daemon_type", "dnsdist" },
+    { "version", VERSION},
+    { "servers", servers},
+    { "frontends", frontends },
+    { "pools", pools },
+    { "rules", rules},
+    { "response-rules", responseRules},
+    { "cache-hit-response-rules", cacheHitResponseRules},
+    { "self-answered-response-rules", selfAnsweredResponseRules},
+    { "acl", acl},
+    { "local", localaddressesStr},
+    { "dohFrontends", dohs }
+  };
+  resp.headers["Content-Type"] = "application/json";
+  resp.body = my_json.dump();
+}
 
-        output << "# HELP " << frontsbase << "http_connects " << "Number of DoH TCP connections established to this frontend" << "\n";
-        output << "# TYPE " << frontsbase << "http_connects " << "counter" << "\n";
+static void handleStatsOnly(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  handleCORS(req, resp);
+  resp.status = 200;
+
+  Json::array doc;
+  for(const auto& item : g_stats.entries) {
+    if (item.first == "special-memory-usage")
+      continue; // Too expensive for get-all
+
+    if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
+      doc.push_back(Json::object {
+          { "type", "StatisticItem" },
+          { "name", item.first },
+          { "value", (double)(*val)->load() }
+        });
+    }
+    else if (const auto& dval = boost::get<double*>(&item.second)) {
+      doc.push_back(Json::object {
+          { "type", "StatisticItem" },
+          { "name", item.first },
+          { "value", (**dval) }
+        });
+    }
+    else {
+      doc.push_back(Json::object {
+          { "type", "StatisticItem" },
+          { "name", item.first },
+          { "value", (double)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
+        });
+    }
+  }
+  Json my_json = doc;
+  resp.body = my_json.dump();
+  resp.headers["Content-Type"] = "application/json";
+}
 
-        output << "# HELP " << frontsbase << "doh_http_method_queries " << "Number of DoH queries received by dnsdist, by HTTP method" << "\n";
-        output << "# TYPE " << frontsbase << "doh_http_method_queries " << "counter" << "\n";
+static void handleConfigDump(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  handleCORS(req, resp);
+  resp.status = 200;
+
+  Json::array doc;
+  typedef boost::variant<bool, double, std::string> configentry_t;
+  std::vector<std::pair<std::string, configentry_t> > configEntries {
+    { "acl", g_ACL.getLocal()->toString() },
+    { "allow-empty-response", g_allowEmptyResponse },
+    { "control-socket", g_serverControl.toStringWithPort() },
+    { "ecs-override", g_ECSOverride },
+    { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
+    { "ecs-source-prefix-v6", (double)  g_ECSSourcePrefixV6 },
+    { "fixup-case", g_fixupCase },
+    { "max-outstanding", (double) g_maxOutstanding },
+    { "server-policy", g_policy.getLocal()->getName() },
+    { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
+    { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
+    { "tcp-send-timeout", (double) g_tcpSendTimeout },
+    { "truncate-tc", g_truncateTC },
+    { "verbose", g_verbose },
+    { "verbose-health-checks", g_verboseHealthChecks }
+  };
+  for(const auto& item : configEntries) {
+    if (const auto& bval = boost::get<bool>(&item.second)) {
+      doc.push_back(Json::object {
+          { "type", "ConfigSetting" },
+          { "name", item.first },
+          { "value", *bval }
+        });
+    }
+    else if (const auto& sval = boost::get<string>(&item.second)) {
+      doc.push_back(Json::object {
+          { "type", "ConfigSetting" },
+          { "name", item.first },
+          { "value", *sval }
+        });
+    }
+    else if (const auto& dval = boost::get<double>(&item.second)) {
+      doc.push_back(Json::object {
+          { "type", "ConfigSetting" },
+          { "name", item.first },
+          { "value", *dval }
+        });
+    }
+  }
+  Json my_json = doc;
+  resp.body = my_json.dump();
+  resp.headers["Content-Type"] = "application/json";
+}
 
-        output << "# HELP " << frontsbase << "doh_http_version_queries " << "Number of DoH queries received by dnsdist, by HTTP version" << "\n";
-        output << "# TYPE " << frontsbase << "doh_http_version_queries " << "counter" << "\n";
+static void handleAllowFrom(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  handleCORS(req, resp);
 
-        output << "# HELP " << frontsbase << "doh_bad_requests " << "Number of requests that could not be converted to a DNS query" << "\n";
-        output << "# TYPE " << frontsbase << "doh_bad_requests " << "counter" << "\n";
+  resp.headers["Content-Type"] = "application/json";
+  resp.status = 200;
 
-        output << "# HELP " << frontsbase << "doh_responses " << "Number of responses sent, by type" << "\n";
-        output << "# TYPE " << frontsbase << "doh_responses " << "counter" << "\n";
+  if (req.method == "PUT") {
+    std::string err;
+    Json doc = Json::parse(req.body, err);
 
-        output << "# HELP " << frontsbase << "doh_version_status_responses " << "Number of requests that could not be converted to a DNS query" << "\n";
-        output << "# TYPE " << frontsbase << "doh_version_status_responses " << "counter" << "\n";
+    if (!doc.is_null()) {
+      NetmaskGroup nmg;
+      auto aclList = doc["value"];
+      if (aclList.is_array()) {
 
-#ifdef HAVE_DNS_OVER_HTTPS
-        std::map<std::string,uint64_t> dohFrontendDuplicates;
-        for(const auto& doh : g_dohlocals) {
-          const string frontName = doh->d_local.toStringWithPort();
-          uint64_t threadNumber = 0;
-          auto dupPair = frontendDuplicates.insert({frontName, 1});
-          if (!dupPair.second) {
-            threadNumber = dupPair.first->second;
-            ++(dupPair.first->second);
+        for (auto value : aclList.array_items()) {
+          try {
+            nmg.addMask(value.string_value());
+          } catch (NetmaskException &e) {
+            resp.status = 400;
+            break;
           }
-          const std::string addrlabel = boost::str(boost::format("frontend=\"%1%\",thread=\"%2%\"") % frontName % threadNumber);
-          const std::string label = "{" + addrlabel + "} ";
-
-          output << frontsbase << "http_connects" << label << doh->d_httpconnects << "\n";
-          output << frontsbase << "doh_http_method_queries{method=\"get\"," << addrlabel << "} " << doh->d_getqueries << "\n";
-          output << frontsbase << "doh_http_method_queries{method=\"post\"," << addrlabel << "} " << doh->d_postqueries << "\n";
-
-          output << frontsbase << "doh_http_version_queries{version=\"1\"," << addrlabel << "} " << doh->d_http1Stats.d_nbQueries << "\n";
-          output << frontsbase << "doh_http_version_queries{version=\"2\"," << addrlabel << "} " << doh->d_http2Stats.d_nbQueries << "\n";
-
-          output << frontsbase << "doh_bad_requests{" << addrlabel << "} " << doh->d_badrequests << "\n";
-
-          output << frontsbase << "doh_responses{type=\"error\"," << addrlabel << "} " << doh->d_errorresponses << "\n";
-          output << frontsbase << "doh_responses{type=\"redirect\"," << addrlabel << "} " << doh->d_redirectresponses << "\n";
-          output << frontsbase << "doh_responses{type=\"valid\"," << addrlabel << "} " << doh->d_validresponses << "\n";
-
-          output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"200\"," << addrlabel << "} " << doh->d_http1Stats.d_nb200Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"400\"," << addrlabel << "} " << doh->d_http1Stats.d_nb400Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"403\"," << addrlabel << "} " << doh->d_http1Stats.d_nb403Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"500\"," << addrlabel << "} " << doh->d_http1Stats.d_nb500Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"502\"," << addrlabel << "} " << doh->d_http1Stats.d_nb502Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"other\"," << addrlabel << "} " << doh->d_http1Stats.d_nbOtherResponses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"200\"," << addrlabel << "} " << doh->d_http2Stats.d_nb200Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"400\"," << addrlabel << "} " << doh->d_http2Stats.d_nb400Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"403\"," << addrlabel << "} " << doh->d_http2Stats.d_nb403Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"500\"," << addrlabel << "} " << doh->d_http2Stats.d_nb500Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"502\"," << addrlabel << "} " << doh->d_http2Stats.d_nb502Responses << "\n";
-          output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"other\"," << addrlabel << "} " << doh->d_http2Stats.d_nbOtherResponses << "\n";
         }
-#endif /* HAVE_DNS_OVER_HTTPS */
 
-        auto localPools = g_pools.getLocal();
-        const string cachebase = "dnsdist_pool_";
-        output << "# HELP dnsdist_pool_servers " << "Number of servers in that pool" << "\n";
-        output << "# TYPE dnsdist_pool_servers " << "gauge" << "\n";
-        output << "# HELP dnsdist_pool_active_servers " << "Number of available servers in that pool" << "\n";
-        output << "# TYPE dnsdist_pool_active_servers " << "gauge" << "\n";
-
-        output << "# HELP dnsdist_pool_cache_size " << "Maximum number of entries that this cache can hold" << "\n";
-        output << "# TYPE dnsdist_pool_cache_size " << "gauge" << "\n";
-        output << "# HELP dnsdist_pool_cache_entries " << "Number of entries currently present in that cache" << "\n";
-        output << "# TYPE dnsdist_pool_cache_entries " << "gauge" << "\n";
-        output << "# HELP dnsdist_pool_cache_hits " << "Number of hits from that cache" << "\n";
-        output << "# TYPE dnsdist_pool_cache_hits " << "counter" << "\n";
-        output << "# HELP dnsdist_pool_cache_misses " << "Number of misses from that cache" << "\n";
-        output << "# TYPE dnsdist_pool_cache_misses " << "counter" << "\n";
-        output << "# HELP dnsdist_pool_cache_deferred_inserts " << "Number of insertions into that cache skipped because it was already locked" << "\n";
-        output << "# TYPE dnsdist_pool_cache_deferred_inserts " << "counter" << "\n";
-        output << "# HELP dnsdist_pool_cache_deferred_lookups " << "Number of lookups into that cache skipped because it was already locked" << "\n";
-        output << "# TYPE dnsdist_pool_cache_deferred_lookups " << "counter" << "\n";
-        output << "# HELP dnsdist_pool_cache_lookup_collisions " << "Number of lookups into that cache that triggered a collision (same hash but different entry)" << "\n";
-        output << "# TYPE dnsdist_pool_cache_lookup_collisions " << "counter" << "\n";
-        output << "# HELP dnsdist_pool_cache_insert_collisions " << "Number of insertions into that cache that triggered a collision (same hash but different entry)" << "\n";
-        output << "# TYPE dnsdist_pool_cache_insert_collisions " << "counter" << "\n";
-        output << "# HELP dnsdist_pool_cache_ttl_too_shorts " << "Number of insertions into that cache skipped because the TTL of the answer was not long enough" << "\n";
-        output << "# TYPE dnsdist_pool_cache_ttl_too_shorts " << "counter" << "\n";
-
-        for (const auto& entry : *localPools) {
-          string poolName = entry.first;
-
-          if (poolName.empty()) {
-            poolName = "_default_";
-          }
-          const string label = "{pool=\"" + poolName + "\"}";
-          const std::shared_ptr<ServerPool> pool = entry.second;
-          output << "dnsdist_pool_servers" << label << " " << pool->countServers(false) << "\n";
-          output << "dnsdist_pool_active_servers" << label << " " << pool->countServers(true) << "\n";
-
-          if (pool->packetCache != nullptr) {
-            const auto& cache = pool->packetCache;
-
-            output << cachebase << "cache_size"              <<label << " " << cache->getMaxEntries()       << "\n";
-            output << cachebase << "cache_entries"           <<label << " " << cache->getEntriesCount()     << "\n";
-            output << cachebase << "cache_hits"              <<label << " " << cache->getHits()             << "\n";
-            output << cachebase << "cache_misses"            <<label << " " << cache->getMisses()           << "\n";
-            output << cachebase << "cache_deferred_inserts"  <<label << " " << cache->getDeferredInserts()  << "\n";
-            output << cachebase << "cache_deferred_lookups"  <<label << " " << cache->getDeferredLookups()  << "\n";
-            output << cachebase << "cache_lookup_collisions" <<label << " " << cache->getLookupCollisions() << "\n";
-            output << cachebase << "cache_insert_collisions" <<label << " " << cache->getInsertCollisions() << "\n";
-            output << cachebase << "cache_ttl_too_shorts"    <<label << " " << cache->getTTLTooShorts()     << "\n";
-          }
+        if (resp.status == 200) {
+          infolog("Updating the ACL via the API to %s", nmg.toString());
+          g_ACL.setState(nmg);
+          apiSaveACL(nmg);
         }
+      }
+      else {
+        resp.status = 400;
+      }
+    }
+    else {
+      resp.status = 400;
+    }
+  }
+  if (resp.status == 200) {
+    Json::array acl;
+    vector<string> vec;
+    g_ACL.getLocal()->toStringVector(&vec);
 
-        output << "# HELP dnsdist_info " << "Info from dnsdist, value is always 1" << "\n";
-        output << "# TYPE dnsdist_info " << "gauge" << "\n";
-        output << "dnsdist_info{version=\"" << VERSION << "\"} " << "1" << "\n";
-
-        resp.body = output.str();
-        resp.headers["Content-Type"] = "text/plain";
+    for(const auto& s : vec) {
+      acl.push_back(s);
     }
 
-    else if(req.url.path=="/api/v1/servers/localhost") {
-      handleCORS(req, resp);
-      resp.status=200;
-
-      Json::array servers;
-      auto localServers = g_dstates.getLocal();
-      int num=0;
-      for(const auto& a : *localServers) {
-       string status;
-       if(a->availability == DownstreamState::Availability::Up)
-         status = "UP";
-       else if(a->availability == DownstreamState::Availability::Down)
-         status = "DOWN";
-       else
-         status = (a->upStatus ? "up" : "down");
-
-       Json::array pools;
-       for(const auto& p: a->pools)
-         pools.push_back(p);
-
-       Json::object server{
-         {"id", num++},
-         {"name", a->getName()},
-          {"address", a->remote.toStringWithPort()},
-          {"state", status},
-          {"qps", (double)a->queryLoad},
-          {"qpsLimit", (double)a->qps.getRate()},
-          {"outstanding", (double)a->outstanding},
-          {"reuseds", (double)a->reuseds},
-          {"weight", (double)a->weight},
-          {"order", (double)a->order},
-          {"pools", pools},
-          {"latency", (double)(a->latencyUsec/1000.0)},
-          {"queries", (double)a->queries},
-          {"responses", (double)a->responses},
-          {"sendErrors", (double)a->sendErrors},
-          {"tcpDiedSendingQuery", (double)a->tcpDiedSendingQuery},
-          {"tcpDiedReadingResponse", (double)a->tcpDiedReadingResponse},
-          {"tcpGaveUp", (double)a->tcpGaveUp},
-          {"tcpReadTimeouts", (double)a->tcpReadTimeouts},
-          {"tcpWriteTimeouts", (double)a->tcpWriteTimeouts},
-          {"tcpCurrentConnections", (double)a->tcpCurrentConnections},
-          {"tcpAvgQueriesPerConnection", (double)a->tcpAvgQueriesPerConnection},
-          {"tcpAvgConnectionDuration", (double)a->tcpAvgConnectionDuration},
-          {"dropRate", (double)a->dropRate}
-        };
+    Json::object obj{
+      { "type", "ConfigSetting" },
+      { "name", "allow-from" },
+      { "value", acl }
+    };
+    Json my_json = obj;
+    resp.body = my_json.dump();
+  }
+}
 
-        /* sending a latency for a DOWN server doesn't make sense */
-        if (a->availability == DownstreamState::Availability::Down) {
-          server["latency"] = nullptr;
-        }
+static std::unordered_map<std::string, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)>> s_webHandlers;
 
-       servers.push_back(server);
-      }
+void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> handler);
 
-      Json::array frontends;
-      num=0;
-      for(const auto& front : g_frontends) {
-        if (front->udpFD == -1 && front->tcpFD == -1)
-          continue;
-        Json::object frontend{
-          { "id", num++ },
-          { "address", front->local.toStringWithPort() },
-          { "udp", front->udpFD >= 0 },
-          { "tcp", front->tcpFD >= 0 },
-          { "type", front->getType() },
-          { "queries", (double) front->queries.load() },
-          { "responses", (double) front->responses.load() },
-          { "tcpDiedReadingQuery", (double) front->tcpDiedReadingQuery.load() },
-          { "tcpDiedSendingResponse", (double) front->tcpDiedSendingResponse.load() },
-          { "tcpGaveUp", (double) front->tcpGaveUp.load() },
-          { "tcpClientTimeouts", (double) front->tcpClientTimeouts },
-          { "tcpDownstreamTimeouts", (double) front->tcpDownstreamTimeouts },
-          { "tcpCurrentConnections", (double) front->tcpCurrentConnections },
-          { "tcpAvgQueriesPerConnection", (double) front->tcpAvgQueriesPerConnection },
-          { "tcpAvgConnectionDuration", (double) front->tcpAvgConnectionDuration },
-          { "tlsNewSessions", (double) front->tlsNewSessions },
-          { "tlsResumptions", (double) front->tlsResumptions },
-          { "tlsUnknownTicketKey", (double) front->tlsUnknownTicketKey },
-          { "tlsInactiveTicketKey", (double) front->tlsInactiveTicketKey },
-          { "tls10Queries", (double) front->tls10queries },
-          { "tls11Queries", (double) front->tls11queries },
-          { "tls12Queries", (double) front->tls12queries },
-          { "tls13Queries", (double) front->tls13queries },
-          { "tlsUnknownQueries", (double) front->tlsUnknownqueries },
-        };
-        const TLSErrorCounters* errorCounters = nullptr;
-        if (front->tlsFrontend != nullptr) {
-          errorCounters = &front->tlsFrontend->d_tlsCounters;
-        }
-        else if (front->dohFrontend != nullptr) {
-          errorCounters = &front->dohFrontend->d_tlsCounters;
-        }
-        if (errorCounters != nullptr) {
-          frontend["tlsHandshakeFailuresDHKeyTooSmall"] = (double)errorCounters->d_dhKeyTooSmall;
-          frontend["tlsHandshakeFailuresInappropriateFallBack"] = (double)errorCounters->d_inappropriateFallBack;
-          frontend["tlsHandshakeFailuresNoSharedCipher"] = (double)errorCounters->d_noSharedCipher;
-          frontend["tlsHandshakeFailuresUnknownCipher"] = (double)errorCounters->d_unknownCipherType;
-          frontend["tlsHandshakeFailuresUnknownKeyExchangeType"] = (double)errorCounters->d_unknownKeyExchangeType;
-          frontend["tlsHandshakeFailuresUnknownProtocol"] = (double)errorCounters->d_unknownProtocol;
-          frontend["tlsHandshakeFailuresUnsupportedEC"] = (double)errorCounters->d_unsupportedEC;
-          frontend["tlsHandshakeFailuresUnsupportedProtocol"] = (double)errorCounters->d_unsupportedProtocol;
-        }
-        frontends.push_back(frontend);
-      }
+void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> handler)
+{
+  s_webHandlers[endpoint] = handler;
+}
 
-      Json::array dohs;
-#ifdef HAVE_DNS_OVER_HTTPS
-      {
-        num = 0;
-        for(const auto& doh : g_dohlocals) {
-          Json::object obj{
-            { "id", num++ },
-            { "address", doh->d_local.toStringWithPort() },
-            { "http-connects", (double) doh->d_httpconnects },
-            { "http1-queries", (double) doh->d_http1Stats.d_nbQueries },
-            { "http2-queries", (double) doh->d_http2Stats.d_nbQueries },
-            { "http1-200-responses", (double) doh->d_http1Stats.d_nb200Responses },
-            { "http2-200-responses", (double) doh->d_http2Stats.d_nb200Responses },
-            { "http1-400-responses", (double) doh->d_http1Stats.d_nb400Responses },
-            { "http2-400-responses", (double) doh->d_http2Stats.d_nb400Responses },
-            { "http1-403-responses", (double) doh->d_http1Stats.d_nb403Responses },
-            { "http2-403-responses", (double) doh->d_http2Stats.d_nb403Responses },
-            { "http1-500-responses", (double) doh->d_http1Stats.d_nb500Responses },
-            { "http2-500-responses", (double) doh->d_http2Stats.d_nb500Responses },
-            { "http1-502-responses", (double) doh->d_http1Stats.d_nb502Responses },
-            { "http2-502-responses", (double) doh->d_http2Stats.d_nb502Responses },
-            { "http1-other-responses", (double) doh->d_http1Stats.d_nbOtherResponses },
-            { "http2-other-responses", (double) doh->d_http2Stats.d_nbOtherResponses },
-            { "get-queries", (double) doh->d_getqueries },
-            { "post-queries", (double) doh->d_postqueries },
-            { "bad-requests", (double) doh->d_badrequests },
-            { "error-responses", (double) doh->d_errorresponses },
-            { "redirect-responses", (double) doh->d_redirectresponses },
-            { "valid-responses", (double) doh->d_validresponses }
-          };
-          dohs.push_back(obj);
-        }
-      }
-#endif /* HAVE_DNS_OVER_HTTPS */
+static void redirectToIndex(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  const string charset = "; charset=utf-8";
+  resp.body.assign(s_urlmap.at("index.html"));
+  resp.headers["Content-Type"] = "text/html" + charset;
+  resp.status = 200;
+}
 
-      Json::array pools;
-      auto localPools = g_pools.getLocal();
-      num=0;
-      for(const auto& pool : *localPools) {
-        const auto& cache = pool.second->packetCache;
-        Json::object entry {
-          { "id", num++ },
-          { "name", pool.first },
-          { "serversCount", (double) pool.second->countServers(false) },
-          { "cacheSize", (double) (cache ? cache->getMaxEntries() : 0) },
-          { "cacheEntries", (double) (cache ? cache->getEntriesCount() : 0) },
-          { "cacheHits", (double) (cache ? cache->getHits() : 0) },
-          { "cacheMisses", (double) (cache ? cache->getMisses() : 0) },
-          { "cacheDeferredInserts", (double) (cache ? cache->getDeferredInserts() : 0) },
-          { "cacheDeferredLookups", (double) (cache ? cache->getDeferredLookups() : 0) },
-          { "cacheLookupCollisions", (double) (cache ? cache->getLookupCollisions() : 0) },
-          { "cacheInsertCollisions", (double) (cache ? cache->getInsertCollisions() : 0) },
-          { "cacheTTLTooShorts", (double) (cache ? cache->getTTLTooShorts() : 0) }
-        };
-        pools.push_back(entry);
-      }
+static void handleBuiltInFiles(const YaHTTP::Request& req, YaHTTP::Response& resp)
+{
+  if (req.url.path.empty() || !s_urlmap.count(req.url.path.c_str()+1)) {
+    resp.status = 404;
+    return;
+  }
 
-      Json::array rules;
-      auto localRules = g_rulactions.getLocal();
-      num=0;
-      for(const auto& a : *localRules) {
-       Json::object rule{
-          {"id", num++},
-          {"creationOrder", (double)a.d_creationOrder},
-          {"uuid", boost::uuids::to_string(a.d_id)},
-          {"matches", (double)a.d_rule->d_matches},
-          {"rule", a.d_rule->toString()},
-          {"action", a.d_action->toString()},
-          {"action-stats", a.d_action->getStats()}
-        };
-       rules.push_back(rule);
-      }
+  resp.body.assign(s_urlmap.at(req.url.path.c_str()+1));
 
-      auto responseRules = someResponseRulesToJson(&g_resprulactions);
-      auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitresprulactions);
-      auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredresprulactions);
+  vector<string> parts;
+  stringtok(parts, req.url.path, ".");
+  static const std::unordered_map<std::string, std::string> contentTypeMap = {
+    { "html", "text/html" },
+    { "css", "text/css" },
+    { "js", "application/javascript" },
+    { "png", "image/png" },
+  };
 
-      string acl;
+  const auto& it = contentTypeMap.find(parts.back());
+  if (it != contentTypeMap.end()) {
+    const string charset = "; charset=utf-8";
+    resp.headers["Content-Type"] = it->second + charset;
+  }
 
-      vector<string> vec;
-      g_ACL.getLocal()->toStringVector(&vec);
+  resp.status = 200;
+}
 
-      for(const auto& s : vec) {
-        if(!acl.empty()) acl += ", ";
-        acl+=s;
-      }
-      string localaddressesStr;
-      std::set<std::string> localaddresses;
-      for(const auto& front : g_frontends) {
-        localaddresses.insert(front->local.toStringWithPort());
-      }
-      for (const auto& addr : localaddresses) {
-        if (!localaddressesStr.empty()) {
-          localaddressesStr += ", ";
-        }
-        localaddressesStr += addr;
-      }
+void registerBuiltInWebHandlers()
+{
+  registerWebHandler("/jsonstat", handleJSONStats);
+  registerWebHandler("/metrics", handlePrometheus);
+  registerWebHandler("/api/v1/servers/localhost", handleStats);
+  registerWebHandler("/api/v1/servers/localhost/statistics", handleStatsOnly);
+  registerWebHandler("/api/v1/servers/localhost/config", handleConfigDump);
+  registerWebHandler("/api/v1/servers/localhost/config/allow-from", handleAllowFrom);
+  registerWebHandler("/", redirectToIndex);
+
+  for (const auto& path : s_urlmap) {
+    registerWebHandler("/" + path.first, handleBuiltInFiles);
+  }
+}
 
-      Json my_json = Json::object {
-        { "daemon_type", "dnsdist" },
-        { "version", VERSION},
-        { "servers", servers},
-        { "frontends", frontends },
-        { "pools", pools },
-        { "rules", rules},
-        { "response-rules", responseRules},
-        { "cache-hit-response-rules", cacheHitResponseRules},
-        { "self-answered-response-rules", selfAnsweredResponseRules},
-        { "acl", acl},
-        { "local", localaddressesStr},
-        { "dohFrontends", dohs }
-      };
-      resp.headers["Content-Type"] = "application/json";
-      resp.body=my_json.dump();
-    }
-    else if(req.url.path=="/api/v1/servers/localhost/statistics") {
-      handleCORS(req, resp);
-      resp.status=200;
-
-      Json::array doc;
-      for(const auto& item : g_stats.entries) {
-        if (item.first == "special-memory-usage")
-          continue; // Too expensive for get-all
-
-        if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
-          doc.push_back(Json::object {
-              { "type", "StatisticItem" },
-              { "name", item.first },
-              { "value", (double)(*val)->load() }
-            });
-        }
-        else if (const auto& dval = boost::get<double*>(&item.second)) {
-          doc.push_back(Json::object {
-              { "type", "StatisticItem" },
-              { "name", item.first },
-              { "value", (**dval) }
-            });
-        }
-        else {
-          doc.push_back(Json::object {
-              { "type", "StatisticItem" },
-              { "name", item.first },
-              { "value", (double)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
-            });
-        }
-      }
-      Json my_json = doc;
-      resp.body=my_json.dump();
-      resp.headers["Content-Type"] = "application/json";
-    }
-    else if(req.url.path=="/api/v1/servers/localhost/config") {
-      handleCORS(req, resp);
-      resp.status=200;
-
-      Json::array doc;
-      typedef boost::variant<bool, double, std::string> configentry_t;
-      std::vector<std::pair<std::string, configentry_t> > configEntries {
-        { "acl", g_ACL.getLocal()->toString() },
-        { "allow-empty-response", g_allowEmptyResponse },
-        { "control-socket", g_serverControl.toStringWithPort() },
-        { "ecs-override", g_ECSOverride },
-        { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
-        { "ecs-source-prefix-v6", (double)  g_ECSSourcePrefixV6 },
-        { "fixup-case", g_fixupCase },
-        { "max-outstanding", (double) g_maxOutstanding },
-        { "server-policy", g_policy.getLocal()->getName() },
-        { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
-        { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
-        { "tcp-send-timeout", (double) g_tcpSendTimeout },
-        { "truncate-tc", g_truncateTC },
-        { "verbose", g_verbose },
-        { "verbose-health-checks", g_verboseHealthChecks }
-      };
-      for(const auto& item : configEntries) {
-        if (const auto& bval = boost::get<bool>(&item.second)) {
-          doc.push_back(Json::object {
-              { "type", "ConfigSetting" },
-              { "name", item.first },
-              { "value", *bval }
-          });
-        }
-        else if (const auto& sval = boost::get<string>(&item.second)) {
-          doc.push_back(Json::object {
-              { "type", "ConfigSetting" },
-              { "name", item.first },
-              { "value", *sval }
-          });
-        }
-        else if (const auto& dval = boost::get<double>(&item.second)) {
-          doc.push_back(Json::object {
-              { "type", "ConfigSetting" },
-              { "name", item.first },
-              { "value", *dval }
-          });
-        }
+static void connectionThread(int sockFD, ComboAddress remote)
+{
+  setThreadName("dnsdist/webConn");
+
+  vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
+
+  Socket sock(sockFD);
+  sockFD = -1;
+
+  try {
+    YaHTTP::AsyncRequestLoader yarl;
+    YaHTTP::Request req;
+    bool finished = false;
+
+    yarl.initialize(&req);
+    while (!finished) {
+      int bytes;
+      char buf[1024];
+      bytes = read(sock.getHandle(), buf, sizeof(buf));
+      if (bytes > 0) {
+        string data = string(buf, bytes);
+        finished = yarl.feed(data);
+      } else {
+        // read error OR EOF
+        break;
       }
-      Json my_json = doc;
-      resp.body=my_json.dump();
-      resp.headers["Content-Type"] = "application/json";
     }
-    else if(req.url.path=="/api/v1/servers/localhost/config/allow-from") {
-      handleCORS(req, resp);
+    yarl.finalize();
 
-      resp.headers["Content-Type"] = "application/json";
-      resp.status=200;
-
-      if (req.method == "PUT") {
-        std::string err;
-        Json doc = Json::parse(req.body, err);
-
-        if (!doc.is_null()) {
-          NetmaskGroup nmg;
-          auto aclList = doc["value"];
-          if (aclList.is_array()) {
-
-            for (auto value : aclList.array_items()) {
-              try {
-                nmg.addMask(value.string_value());
-              } catch (NetmaskException &e) {
-                resp.status = 400;
-                break;
-              }
-            }
-
-            if (resp.status == 200) {
-              infolog("Updating the ACL via the API to %s", nmg.toString());
-              g_ACL.setState(nmg);
-              apiSaveACL(nmg);
-            }
-          }
-          else {
-            resp.status = 400;
-          }
-        }
-        else {
-          resp.status = 400;
-        }
-      }
-      if (resp.status == 200) {
-        Json::array acl;
-        vector<string> vec;
-        g_ACL.getLocal()->toStringVector(&vec);
+    req.getvars.erase("_"); // jQuery cache buster
 
-        for(const auto& s : vec) {
-          acl.push_back(s);
-        }
+    YaHTTP::Response resp;
+    resp.version = req.version;
 
-        Json::object obj{
-          { "type", "ConfigSetting" },
-          { "name", "allow-from" },
-          { "value", acl }
-        };
-        Json my_json = obj;
-        resp.body=my_json.dump();
-      }
+    {
+      std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
+
+      addCustomHeaders(resp, g_webserverConfig.customHeaders);
+      addSecurityHeaders(resp, g_webserverConfig.customHeaders);
     }
-    else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
-      resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
-      vector<string> parts;
-      stringtok(parts, req.url.path, ".");
-      if(parts.back() == "html")
-        resp.headers["Content-Type"] = "text/html" + charset;
-      else if(parts.back() == "css")
-        resp.headers["Content-Type"] = "text/css" + charset;
-      else if(parts.back() == "js")
-        resp.headers["Content-Type"] = "application/javascript" + charset;
-      else if(parts.back() == "png")
-        resp.headers["Content-Type"] = "image/png";
-      resp.status=200;
+    /* indicate that the connection will be closed after completion of the response */
+    resp.headers["Connection"] = "close";
+
+    /* no need to send back the API key if any */
+    resp.headers.erase("X-API-Key");
+
+    if (req.method == "OPTIONS") {
+      /* the OPTIONS method should not require auth, otherwise it breaks CORS */
+      handleCORS(req, resp);
+      resp.status = 200;
+    }
+    else if (!compareAuthorization(req)) {
+      YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
+      if (header != req.headers.end()) {
+        errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, remote.toStringWithPort());
+      }
+      resp.status = 401;
+      resp.body = "<h1>Unauthorized</h1>";
+      resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
     }
-    else if(req.url.path=="/") {
-      resp.body.assign(g_urlmap["index.html"]);
-      resp.headers["Content-Type"] = "text/html" + charset;
-      resp.status=200;
+    else if (!isMethodAllowed(req)) {
+      resp.status = 405;
     }
     else {
-      // cerr<<"404 for: "<<req.url.path<<endl;
-      resp.status=404;
+      const auto it = s_webHandlers.find(req.url.path);
+      if (it != s_webHandlers.end()) {
+        it->second(req, resp);
+      }
+      else {
+        resp.status = 404;
+      }
     }
 
     std::ostringstream ofs;
     ofs << resp;
-    string done;
-    done=ofs.str();
-    writen2(sock, done.c_str(), done.size());
-
-    close(sock);
-    sock = -1;
+    string done = ofs.str();
+    writen2(sock.getHandle(), done.c_str(), done.size());
   }
-  catch(const YaHTTP::ParseError& e) {
+  catch (const YaHTTP::ParseError& e) {
     vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
-    close(sock);
   }
-  catch(const std::exception& e) {
+  catch (const std::exception& e) {
     errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
-    close(sock);
   }
-  catch(...) {
+  catch (...) {
     errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
-    close(sock);
   }
 }
 
index 953bfbe2fd233d0119213618b616979111ea6956..f8ef4e2e951fb680d6c041e3ecf4da938acb9243 100644 (file)
@@ -53,6 +53,7 @@
 #include "dnsdist-proxy-protocol.hh"
 #include "dnsdist-rings.hh"
 #include "dnsdist-secpoll.hh"
+#include "dnsdist-web.hh"
 #include "dnsdist-xpf.hh"
 
 #include "base64.hh"
@@ -2188,6 +2189,7 @@ try
     consoleACL.addMask(mask);
   }
   g_consoleACL.setState(consoleACL);
+  registerBuiltInWebHandlers();
 
   if (g_cmdLine.checkConfig) {
     setupLua(g_lua, false, true, g_cmdLine.config);
index 3b347b5c54763652b1d53ae218670663095967b0..d52971f5ac629e39bb2615b1ca0d2ed91a03c22b 100644 (file)
@@ -148,6 +148,7 @@ dnsdist_SOURCES = \
        dnsdist-lua-inspection.cc \
        dnsdist-lua-rules.cc \
        dnsdist-lua-vars.cc \
+       dnsdist-lua-web.cc \
        dnsdist-lua.hh dnsdist-lua.cc \
        dnsdist-prometheus.hh \
        dnsdist-protobuf.cc dnsdist-protobuf.hh \
diff --git a/pdns/dnsdistdist/dnsdist-lua-web.cc b/pdns/dnsdistdist/dnsdist-lua-web.cc
new file mode 100644 (file)
index 0000000..4b5c20b
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#include <yahttp/yahttp.hpp>
+
+#include "dnsdist.hh"
+#include "dnsdist-lua.hh"
+#include "dnsdist-web.hh"
+
+void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> handler);
+
+void setupLuaWeb(LuaContext& luaCtx)
+{
+  luaCtx.writeFunction("registerWebHandler", [](const std::string& path, std::function<void(const YaHTTP::Request*, YaHTTP::Response*)> handler) {
+    /* LuaWrapper does a copy for objects passed by reference, so we pass a pointer */
+    registerWebHandler(path, [handler](const YaHTTP::Request& req, YaHTTP::Response& resp) { handler(&req, &resp); });
+  });
+
+  luaCtx.registerMember<std::string(YaHTTP::Request::*)>("path", [](const YaHTTP::Request& req) -> std::string { return req.url.path; }, [](YaHTTP::Request& req, const std::string& path) { (void) path; });
+  luaCtx.registerMember<int(YaHTTP::Request::*)>("version", [](const YaHTTP::Request& req) -> int { return req.version; }, [](YaHTTP::Request& req, int version) { (void) version; });
+  luaCtx.registerMember<std::string(YaHTTP::Request::*)>("method", [](const YaHTTP::Request& req) -> std::string { return req.method; }, [](YaHTTP::Request& req, const std::string& method) { (void) method; });
+  luaCtx.registerMember<std::string(YaHTTP::Request::*)>("body", [](const YaHTTP::Request& req) -> const std::string { return req.body; }, [](YaHTTP::Request& req, const std::string& body) { (void) body; });
+  luaCtx.registerMember<std::unordered_map<std::string, std::string>(YaHTTP::Request::*)>("getvars", [](const YaHTTP::Request& req) {
+    std::unordered_map<std::string, std::string> values;
+    for (const auto& entry : req.getvars) {
+      values.insert({entry.first, entry.second});
+    }
+    return values;
+  }, [](YaHTTP::Request& req, const std::unordered_map<std::string, std::string>& values) { (void) values; });
+  luaCtx.registerMember<std::unordered_map<std::string, std::string>(YaHTTP::Request::*)>("postvars", [](const YaHTTP::Request& req) {
+    std::unordered_map<std::string, std::string> values;
+    for (const auto& entry : req.postvars) {
+      values.insert({entry.first, entry.second});
+    }
+    return values;
+  }, [](YaHTTP::Request& req, const std::unordered_map<std::string, std::string>& values) { (void) values; });
+  luaCtx.registerMember<std::unordered_map<std::string, std::string>(YaHTTP::Request::*)>("headers", [](const YaHTTP::Request& req) {
+    std::unordered_map<std::string, std::string> values;
+    for (const auto& entry : req.headers) {
+      values.insert({entry.first, entry.second});
+    }
+    return values;
+  }, [](YaHTTP::Request& req, const std::unordered_map<std::string, std::string>& values) { (void) values; });
+
+  /* Response */
+  luaCtx.registerMember<std::string(YaHTTP::Response::*)>("body", [](const YaHTTP::Response& resp) -> const std::string { return resp.body; }, [](YaHTTP::Response& resp, const std::string& body) { resp.body = body; });
+  luaCtx.registerMember<int(YaHTTP::Response::*)>("status", [](const YaHTTP::Response& resp) -> int { return resp.status; }, [](YaHTTP::Response& resp, int status) { resp.status = status; });
+  luaCtx.registerMember<std::unordered_map<std::string, std::string>(YaHTTP::Response::*)>("headers", [](const YaHTTP::Response& resp) {
+    std::unordered_map<std::string, std::string> values;
+    for (const auto& entry : resp.headers) {
+      values.insert({entry.first, entry.second});
+    }
+    return values;
+  }, [](YaHTTP::Response& resp, const std::unordered_map<std::string, std::string>& values) {
+    resp.headers.clear();
+    for (const auto& entry : values) {
+      resp.headers.insert({entry.first, entry.second});
+    }
+  });
+}
+
index 1b5d0015f43f85d69e0bcf757433bee9dd120ece..889139f6d0230bfa5222ba3cdf5a8d3f41c0cc4d 100644 (file)
@@ -20,3 +20,5 @@ void setWebserverACL(const std::string& acl);
 void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders);
 
 void dnsdistWebserverThread(int sock, const ComboAddress& local);
+
+void registerBuiltInWebHandlers();
index 53fc6fa08ab66a1d60202df3afb29d0750712901..084024c50f500f62726a42e999f9c9f8a0c78239 100644 (file)
@@ -349,6 +349,22 @@ Webserver configuration
   * ``custom_headers={[str]=str,...}``: map of string - Allows setting custom headers and removing the defaults.
   * ``acl=newACL``: string - List of IP addresses, as a string, that are allowed to open a connection to the web server. Defaults to "127.0.0.1, ::1".
 
+.. function:: registerWebHandler(path, handler)
+
+  .. versionadded: 1.6.0
+
+  Register a function named ``handler`` that will be called for every query sent to the exact ``path`` path. The function will receive a :class:`WebRequest` object
+  and a :class:`WebResponse` object, representing respectively the HTTP request received and the HTTP response to send.
+  For example an handler registered for '/foo' will receive these queries:
+  - GET /foo
+  - POST /foo
+  - GET /foo?param=1
+  - ...
+  But not queries for /foobar or /foo/bar.
+
+  :param str path: Path to register the handler for.
+  :param function handler: The Lua function to register.
+
 Access Control Lists
 ~~~~~~~~~~~~~~~~~~~~
 
index 0f462f94ec9d10a93a9e78ceebe6d5475f52c943..92ba86af655fa479e9d328ae02744a7506377cbc 100755 (executable)
@@ -23,3 +23,4 @@ These chapters contain extensive information on all functions and object availab
   tuning
   kvs
   logging
+  web
diff --git a/pdns/dnsdistdist/docs/reference/web.rst b/pdns/dnsdistdist/docs/reference/web.rst
new file mode 100644 (file)
index 0000000..f4e975f
--- /dev/null
@@ -0,0 +1,52 @@
+.. _WebObjects:
+
+Webserver-related objects
+=========================
+
+.. class:: WebRequest
+
+  Represent a HTTP query, whose attributes are read-only.
+
+  .. attribute:: WebRequest.body
+
+    The body of this query, as a string.
+
+  .. attribute:: WebRequest.getvars
+
+    The GET parameters of this query, as a table whose keys and values are strings.
+
+  .. attribute:: WebRequest.headers
+
+    The HTTP headers of this query, as a table whose keys and values are strings.
+
+  .. attribute:: WebRequest.method
+
+    The method of this query, as a string.
+
+  .. attribute:: WebRequest.path
+
+    The path of this query, as a string.
+
+  .. attribute:: WebRequest.postvars
+
+    The POST parameters of this query, as a table whose keys and values are strings.
+
+  .. attribute:: WebRequest.version
+
+    The HTTP version of this query, as an integer.
+
+.. class:: WebResponse
+
+  Represent a HTTP response.
+
+  .. attribute:: WebRequest.body
+
+    The body of this response, as a string.
+
+  .. attribute:: WebRequest.headers
+
+    The HTTP headers of this response, as a table whose keys and values are strings.
+
+  .. attribute:: WebRequest.status
+
+    The HTTP status code of this response, as an integer.
index da127415f6dac6661e48ba451864738471bd8c92..9dd02368672010c2aaa1129b81eed83996e72c8a 100755 (executable)
@@ -14,7 +14,7 @@ do
        echo "INCBIN(${c}, \"$a\");"
 done
 
-echo "map<string,string> g_urlmap={"
+echo "static const map<string,string> s_urlmap={"
 for a in $(find ${DIR}html -type f | grep -v \~ | sort)
 do
        b=$(echo $a | sed s:${DIR}html/::g)
index 2869d141becf9d246555ad1a0869850579d52271..8a74451ee4d8a36e235d866af502d07bc96e91ce 100644 (file)
@@ -44,11 +44,11 @@ typedef int ProtocolType; //!< Supported protocol types
 //! Representation of a Socket and many of the Berkeley functions available
 class Socket : public boost::noncopyable
 {
+public:
   Socket(int fd): d_socket(fd)
   {
   }
 
-public:
   //! Construct a socket of specified address family and socket type.
   Socket(int af, int st, ProtocolType pt=0)
   {
index b894323cb4cd59d71272ae91beb0278a455aceb9..34193049455d28903abf17bea57b29c2b5fd198f 100644 (file)
@@ -553,3 +553,61 @@ class TestAPIACL(DNSDistTest):
         r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
         self.assertTrue(r)
         self.assertEquals(r.status_code, 200)
+
+class TestCustomLuaEndpoint(DNSDistTest):
+
+    _webTimeout = 2.0
+    _webServerPort = 8083
+    _webServerBasicAuthPassword = 'secret'
+    _config_template = """
+    setACL({"127.0.0.1/32", "::1/128"})
+    newServer{address="127.0.0.1:%s"}
+    webserver("127.0.0.1:%s", "%s")
+
+    function customHTTPHandler(req, resp)
+      if req.path ~= '/foo' then
+        resp.status = 500
+        return
+      end
+
+      if req.version ~= 11 then
+        resp.status = 501
+        return
+      end
+
+      if req.method ~= 'GET' then
+        resp.status = 502
+        return
+      end
+
+      local get = req.getvars
+      if get['param'] ~= '42' then
+        resp.status = 503
+        return
+      end
+
+      local headers = req.headers
+      if headers['customheader'] ~= 'foobar' then
+        resp.status = 504
+        return
+      end
+
+      resp.body = 'It works!'
+      resp.status = 200
+      resp.headers = { ['Foo']='Bar'}
+    end
+    registerWebHandler('/foo', customHTTPHandler)
+    """
+    _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPassword']
+
+    def testBasic(self):
+        """
+        Custom Web Handler
+        """
+        url = 'http://127.0.0.1:' + str(self._webServerPort) + '/foo?param=42'
+        headers = {'customheader': 'foobar'}
+        r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout, headers=headers)
+        self.assertTrue(r)
+        self.assertEquals(r.status_code, 200)
+        self.assertEquals(r.content, b'It works!')
+        self.assertEquals(r.headers.get('foo'), "Bar")