]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/ws-recursor.cc
rec: ensure correct service user on debian
[thirdparty/pdns.git] / pdns / ws-recursor.cc
CommitLineData
a7a902fb 1/*
6edbf68a
PL
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
870a0fe4
AT
22#ifdef HAVE_CONFIG_H
23#include "config.h"
24#endif
2470b36e 25#include "ws-recursor.hh"
a7a902fb 26#include "json.hh"
fa8fd4d2 27
a7a902fb
BH
28#include <string>
29#include "namespaces.hh"
30#include <iostream>
31#include "iputils.hh"
32#include "rec_channel.hh"
33#include "arguments.hh"
34#include "misc.hh"
8465487d 35#include "syncres.hh"
c89f8cd0 36#include "dnsparser.hh"
639a6287 37#include "json11.hpp"
3ae143b0 38#include "webserver.hh"
6ec5e728 39#include "ws-api.hh"
41942bb3 40#include "logger.hh"
d579380d 41#include "ext/incbin/incbin.h"
2278ffda
PL
42#include "rec-lua-conf.hh"
43#include "rpzloader.hh"
b030ea34 44#include "uuid-utils.hh"
fb4b38f1 45
6dfff36f 46extern thread_local FDMultiplexer* t_fdm;
825fa717 47
639a6287 48using json11::Json;
a7a902fb 49
6ec5e728
CH
50void productServerStatisticsFetch(map<string,string>& out)
51{
72259676 52 map<string,string> stats = getAllStatsMap(StatComponent::API);
6ec5e728
CH
53 out.swap(stats);
54}
55
5376a5d7
RG
56boost::optional<uint64_t> productServerStatisticsFetch(const std::string& name)
57{
58 return getStatByName(name);
59}
60
c348c0c8
CH
61static void apiWriteConfigFile(const string& filebasename, const string& content)
62{
d07bf7ff
PL
63 if (::arg()["api-config-dir"].empty()) {
64 throw ApiException("Config Option \"api-config-dir\" must be set");
c348c0c8
CH
65 }
66
d07bf7ff 67 string filename = ::arg()["api-config-dir"] + "/" + filebasename + ".conf";
c348c0c8
CH
68 ofstream ofconf(filename.c_str());
69 if (!ofconf) {
70 throw ApiException("Could not open config fragment file '"+filename+"' for writing: "+stringerror());
71 }
72 ofconf << "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
73 ofconf << content << endl;
74 ofconf.close();
75}
76
41942bb3
CH
77static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
78{
2054afbb 79 if (req->method == "PUT") {
639a6287 80 Json document = req->json();
41942bb3 81
639a6287
CH
82 auto jlist = document["value"];
83 if (!jlist.is_array()) {
bd0320fe 84 throw ApiException("'value' must be an array");
41942bb3
CH
85 }
86
5f8108e9 87 NetmaskGroup nmg;
639a6287 88 for (auto value : jlist.array_items()) {
faa0f891 89 try {
5f8108e9
RG
90 nmg.addMask(value.string_value());
91 } catch (const NetmaskException &e) {
faa0f891
CH
92 throw ApiException(e.reason);
93 }
94 }
95
41942bb3
CH
96 ostringstream ss;
97
98 // Clear allow-from-file if set, so our changes take effect
99 ss << "allow-from-file=" << endl;
100
101 // Clear allow-from, and provide a "parent" value
102 ss << "allow-from=" << endl;
5f8108e9 103 ss << "allow-from+=" << nmg.toString() << endl;
41942bb3
CH
104
105 apiWriteConfigFile("allow-from", ss.str());
106
107 parseACLs();
108
109 // fall through to GET
110 } else if (req->method != "GET") {
111 throw HttpMethodNotAllowedException();
112 }
113
114 // Return currently configured ACLs
41942bb3
CH
115 vector<string> entries;
116 t_allowFrom->toStringVector(&entries);
117
639a6287
CH
118 resp->setBody(Json::object {
119 { "name", "allow-from" },
120 { "value", entries },
121 });
41942bb3
CH
122}
123
8171ab83 124static void fillZone(const DNSName& zonename, HttpResponse* resp)
02945d9a 125{
a712cb56
RG
126 auto iter = SyncRes::t_sstorage.domainmap->find(zonename);
127 if (iter == SyncRes::t_sstorage.domainmap->end())
86f1af1c 128 throw ApiException("Could not find domain '"+zonename.toLogString()+"'");
02945d9a 129
02945d9a
CH
130 const SyncRes::AuthDomain& zone = iter->second;
131
15cb63ca
CH
132 Json::array servers;
133 for(const ComboAddress& server : zone.d_servers) {
134 servers.push_back(server.toStringWithPort());
02945d9a 135 }
15cb63ca
CH
136
137 Json::array records;
138 for(const SyncRes::AuthDomain::records_t::value_type& dr : zone.d_records) {
139 records.push_back(Json::object {
140 { "name", dr.d_name.toString() },
141 { "type", DNSRecordContent::NumberToType(dr.d_type) },
142 { "ttl", (double)dr.d_ttl },
143 { "content", dr.d_content->getZoneRepresentation() }
144 });
02945d9a 145 }
15cb63ca
CH
146
147 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
148 string zoneId = apiZoneNameToId(iter->first);
149 Json::object doc = {
150 { "id", zoneId },
151 { "url", "/api/v1/servers/localhost/zones/" + zoneId },
152 { "name", iter->first.toString() },
153 { "kind", zone.d_servers.empty() ? "Native" : "Forwarded" },
154 { "servers", servers },
155 { "recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward },
156 { "records", records }
157 };
02945d9a
CH
158
159 resp->setBody(doc);
160}
161
15cb63ca 162static void doCreateZone(const Json document)
02945d9a 163{
d07bf7ff
PL
164 if (::arg()["api-config-dir"].empty()) {
165 throw ApiException("Config Option \"api-config-dir\" must be set");
02945d9a
CH
166 }
167
c576d0c5 168 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
1d6b70f9 169 apiCheckNameAllowedCharacters(zonename.toString());
02945d9a 170
15cb63ca 171 string singleIPTarget = document["single_target_ip"].string_value();
02945d9a
CH
172 string kind = toUpper(stringFromJson(document, "kind"));
173 bool rd = boolFromJson(document, "recursion_desired");
174 string confbasename = "zone-" + apiZoneNameToId(zonename);
175
176 if (kind == "NATIVE") {
177 if (rd)
178 throw ApiException("kind=Native and recursion_desired are mutually exclusive");
10e69330 179 if(!singleIPTarget.empty()) {
180 try {
181 ComboAddress rem(singleIPTarget);
182 if(rem.sin4.sin_family != AF_INET)
183 throw ApiException("");
184 singleIPTarget = rem.toString();
185 }
186 catch(...) {
187 throw ApiException("Single IP target '"+singleIPTarget+"' is invalid");
188 }
189 }
d07bf7ff 190 string zonefilename = ::arg()["api-config-dir"] + "/" + confbasename + ".zone";
02945d9a
CH
191 ofstream ofzone(zonefilename.c_str());
192 if (!ofzone) {
193 throw ApiException("Could not open '"+zonefilename+"' for writing: "+stringerror());
194 }
195 ofzone << "; Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
196 ofzone << zonename << "\tIN\tSOA\tlocal.zone.\thostmaster."<<zonename<<" 1 1 1 1 1" << endl;
7f643957 197 if(!singleIPTarget.empty()) {
10e69330 198 ofzone <<zonename << "\t3600\tIN\tA\t"<<singleIPTarget<<endl;
199 ofzone <<"*."<<zonename << "\t3600\tIN\tA\t"<<singleIPTarget<<endl;
7f643957 200 }
02945d9a
CH
201 ofzone.close();
202
8171ab83 203 apiWriteConfigFile(confbasename, "auth-zones+=" + zonename.toString() + "=" + zonefilename);
02945d9a 204 } else if (kind == "FORWARDED") {
02945d9a 205 string serverlist;
15cb63ca
CH
206 for (auto value : document["servers"].array_items()) {
207 string server = value.string_value();
208 if (server == "") {
209 throw ApiException("Forwarded-to server must not be an empty string");
02945d9a 210 }
5f8108e9
RG
211 try {
212 ComboAddress ca = parseIPAndPort(server, 53);
213 if (!serverlist.empty()) {
214 serverlist += ";";
215 }
216 serverlist += ca.toStringWithPort();
217 } catch (const PDNSException &e) {
218 throw ApiException(e.reason);
15cb63ca 219 }
02945d9a 220 }
15cb63ca
CH
221 if (serverlist == "")
222 throw ApiException("Need at least one upstream server when forwarding");
02945d9a
CH
223
224 if (rd) {
8171ab83 225 apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename.toString() + "=" + serverlist);
02945d9a 226 } else {
8171ab83 227 apiWriteConfigFile(confbasename, "forward-zones+=" + zonename.toString() + "=" + serverlist);
02945d9a
CH
228 }
229 } else {
230 throw ApiException("invalid kind");
231 }
232}
233
8171ab83 234static bool doDeleteZone(const DNSName& zonename)
02945d9a 235{
d07bf7ff
PL
236 if (::arg()["api-config-dir"].empty()) {
237 throw ApiException("Config Option \"api-config-dir\" must be set");
02945d9a
CH
238 }
239
240 string filename;
241
242 // this one must exist
d07bf7ff 243 filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf";
02945d9a
CH
244 if (unlink(filename.c_str()) != 0) {
245 return false;
246 }
247
248 // .zone file is optional
d07bf7ff 249 filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".zone";
02945d9a
CH
250 unlink(filename.c_str());
251
252 return true;
253}
254
255static void apiServerZones(HttpRequest* req, HttpResponse* resp)
256{
2054afbb 257 if (req->method == "POST") {
d07bf7ff
PL
258 if (::arg()["api-config-dir"].empty()) {
259 throw ApiException("Config Option \"api-config-dir\" must be set");
02945d9a
CH
260 }
261
15cb63ca 262 Json document = req->json();
02945d9a 263
c576d0c5 264 DNSName zonename = apiNameToDNSName(stringFromJson(document, "name"));
02945d9a 265
a712cb56
RG
266 auto iter = SyncRes::t_sstorage.domainmap->find(zonename);
267 if (iter != SyncRes::t_sstorage.domainmap->end())
02945d9a
CH
268 throw ApiException("Zone already exists");
269
270 doCreateZone(document);
271 reloadAuthAndForwards();
272 fillZone(zonename, resp);
64a36f0d 273 resp->status = 201;
02945d9a
CH
274 return;
275 }
276
277 if(req->method != "GET")
278 throw HttpMethodNotAllowedException();
279
15cb63ca 280 Json::array doc;
a712cb56 281 for(const SyncRes::domainmap_t::value_type& val : *SyncRes::t_sstorage.domainmap) {
02945d9a 282 const SyncRes::AuthDomain& zone = val.second;
15cb63ca
CH
283 Json::array servers;
284 for(const ComboAddress& server : zone.d_servers) {
285 servers.push_back(server.toStringWithPort());
286 }
02945d9a 287 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
8171ab83 288 string zoneId = apiZoneNameToId(val.first);
15cb63ca
CH
289 doc.push_back(Json::object {
290 { "id", zoneId },
291 { "url", "/api/v1/servers/localhost/zones/" + zoneId },
292 { "name", val.first.toString() },
293 { "kind", zone.d_servers.empty() ? "Native" : "Forwarded" },
294 { "servers", servers },
295 { "recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward }
296 });
02945d9a
CH
297 }
298 resp->setBody(doc);
299}
300
301static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
302{
8171ab83 303 DNSName zonename = apiZoneIdToName(req->parameters["id"]);
02945d9a 304
a712cb56
RG
305 SyncRes::domainmap_t::const_iterator iter = SyncRes::t_sstorage.domainmap->find(zonename);
306 if (iter == SyncRes::t_sstorage.domainmap->end())
86f1af1c 307 throw ApiException("Could not find domain '"+zonename.toLogString()+"'");
02945d9a 308
2054afbb 309 if(req->method == "PUT") {
15cb63ca 310 Json document = req->json();
02945d9a
CH
311
312 doDeleteZone(zonename);
313 doCreateZone(document);
314 reloadAuthAndForwards();
f0e76cee
CH
315 resp->body = "";
316 resp->status = 204; // No Content, but indicate success
02945d9a 317 }
2054afbb 318 else if(req->method == "DELETE") {
02945d9a
CH
319 if (!doDeleteZone(zonename)) {
320 throw ApiException("Deleting domain failed");
321 }
322
323 reloadAuthAndForwards();
324 // empty body on success
325 resp->body = "";
37663c3b 326 resp->status = 204; // No Content: declare that the zone is gone now
02945d9a
CH
327 } else if(req->method == "GET") {
328 fillZone(zonename, resp);
329 } else {
330 throw HttpMethodNotAllowedException();
331 }
332}
333
37bc3d01
CH
334static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
335 if(req->method != "GET")
336 throw HttpMethodNotAllowedException();
337
583ea80d 338 string q = req->getvars["q"];
37bc3d01
CH
339 if (q.empty())
340 throw ApiException("Query q can't be blank");
341
565a3e28 342 Json::array doc;
a712cb56 343 for(const SyncRes::domainmap_t::value_type& val : *SyncRes::t_sstorage.domainmap) {
8171ab83 344 string zoneId = apiZoneNameToId(val.first);
565a3e28
CH
345 string zoneName = val.first.toString();
346 if (pdns_ci_find(zoneName, q) != string::npos) {
347 doc.push_back(Json::object {
348 { "type", "zone" },
349 { "zone_id", zoneId },
350 { "name", zoneName }
351 });
37bc3d01
CH
352 }
353
354 // if zone name is an exact match, don't bother with returning all records/comments in it
8171ab83 355 if (val.first == DNSName(q)) {
37bc3d01
CH
356 continue;
357 }
358
359 const SyncRes::AuthDomain& zone = val.second;
360
565a3e28 361 for(const SyncRes::AuthDomain::records_t::value_type& rr : zone.d_records) {
e325f20c 362 if (pdns_ci_find(rr.d_name.toString(), q) == string::npos && pdns_ci_find(rr.d_content->getZoneRepresentation(), q) == string::npos)
37bc3d01
CH
363 continue;
364
565a3e28
CH
365 doc.push_back(Json::object {
366 { "type", "record" },
367 { "zone_id", zoneId },
368 { "zone_name", zoneName },
369 { "name", rr.d_name.toString() },
370 { "content", rr.d_content->getZoneRepresentation() }
371 });
37bc3d01
CH
372 }
373 }
374 resp->setBody(doc);
375}
376
c0f6a1da 377static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) {
a426cb89
CH
378 if(req->method != "PUT")
379 throw HttpMethodNotAllowedException();
380
c0f6a1da 381 DNSName canon = apiNameToDNSName(req->getvars["domain"]);
46d03ec1 382 bool subtree = (req->getvars.count("subtree") > 0 && req->getvars["subtree"].compare("true") == 0);
c0f6a1da 383
d19c22a1
CHB
384 int count = broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeCache, canon, subtree));
385 count += broadcastAccFunction<uint64_t>(boost::bind(pleaseWipePacketCache, canon, subtree));
386 count += broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon, subtree));
f682752a
CH
387 resp->setBody(Json::object {
388 { "count", count },
389 { "result", "Flushed cache." }
390 });
a426cb89
CH
391}
392
64c9da8c 393static void apiServerRPZStats(HttpRequest* req, HttpResponse* resp) {
2278ffda
PL
394 if(req->method != "GET")
395 throw HttpMethodNotAllowedException();
396
397 auto luaconf = g_luaconfs.getLocal();
398 auto numZones = luaconf->dfe.size();
399
400 Json::object ret;
401
402 for (size_t i=0; i < numZones; i++) {
403 auto zone = luaconf->dfe.getZone(i);
404 if (zone == nullptr)
405 continue;
406 auto name = zone->getName();
20c37dec
PL
407 auto stats = getRPZZoneStats(*name);
408 if (stats == nullptr)
409 continue;
2278ffda 410 Json::object zoneInfo = {
20c37dec
PL
411 {"transfers_failed", (double)stats->d_failedTransfers},
412 {"transfers_success", (double)stats->d_successfulTransfers},
413 {"transfers_full", (double)stats->d_fullTransfers},
414 {"records", (double)stats->d_numberOfRecords},
415 {"last_update", (double)stats->d_lastUpdate},
416 {"serial", (double)stats->d_serial},
2278ffda
PL
417 };
418 ret[*name] = zoneInfo;
419 }
420 resp->setBody(ret);
421}
422
642339d8 423#include "htmlfiles.h"
d579380d 424
b184a9dc 425static void serveStuff(HttpRequest* req, HttpResponse* resp)
d579380d 426{
427 resp->headers["Cache-Control"] = "max-age=86400";
d579380d 428
d887229a 429 if(req->url.path == "/")
430 req->url.path = "/index.html";
431
642339d8 432 const string charset = "; charset=utf-8";
433 if(boost::ends_with(req->url.path, ".html"))
434 resp->headers["Content-Type"] = "text/html" + charset;
435 else if(boost::ends_with(req->url.path, ".css"))
436 resp->headers["Content-Type"] = "text/css" + charset;
437 else if(boost::ends_with(req->url.path,".js"))
438 resp->headers["Content-Type"] = "application/javascript" + charset;
439 else if(boost::ends_with(req->url.path, ".png"))
440 resp->headers["Content-Type"] = "image/png";
441
442 resp->headers["X-Content-Type-Options"] = "nosniff";
443 resp->headers["X-Frame-Options"] = "deny";
444 resp->headers["X-Permitted-Cross-Domain-Policies"] = "none";
445
446 resp->headers["X-XSS-Protection"] = "1; mode=block";
447 // resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
448
449 resp->body = g_urlmap[req->url.path.c_str()+1];
d579380d 450 resp->status = 200;
451}
452
453
1ce57618 454RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
a7a902fb 455{
b0b37121 456 registerAllStats();
a7a902fb 457
d07bf7ff 458 d_ws = new AsyncWebServer(fdm, arg()["webserver-address"], arg().asNum("webserver-port"));
29997a3c 459 d_ws->setApiKey(arg()["api-key"]);
0c3b088c 460 d_ws->setPassword(arg()["webserver-password"]);
8ca656a8 461 d_ws->setLogLevel(arg()["webserver-loglevel"]);
0010aefa
PL
462
463 NetmaskGroup acl;
464 acl.toMasks(::arg()["webserver-allow-from"]);
465 d_ws->setACL(acl);
466
825fa717 467 d_ws->bind();
867f6abc 468
3ae143b0 469 // legacy dispatch
c563cbe5 470 d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2), true);
c0f6a1da 471 d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush);
46d06a12
PL
472 d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-from", &apiServerConfigAllowFrom);
473 d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig);
64c9da8c 474 d_ws->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", &apiServerRPZStats);
46d06a12 475 d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData);
c563cbe5 476 d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics, true);
46d06a12
PL
477 d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail);
478 d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones);
c563cbe5 479 d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail, true);
46d06a12 480 d_ws->registerApiHandler("/api/v1/servers", &apiServer);
9e6d2033 481 d_ws->registerApiHandler("/api", &apiDiscovery);
867f6abc 482
642339d8 483 for(const auto& u : g_urlmap)
484 d_ws->registerWebHandler("/"+u.first, serveStuff);
d579380d 485 d_ws->registerWebHandler("/", serveStuff);
3ae143b0 486 d_ws->go();
867f6abc
CH
487}
488
1ce57618 489void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp)
867f6abc 490{
e2897a7d 491 string command;
a7a902fb 492
583ea80d
CH
493 if(req->getvars.count("command")) {
494 command = req->getvars["command"];
495 req->getvars.erase("command");
3ae143b0 496 }
a7a902fb 497
3ddb9247 498 map<string, string> stats;
a426cb89 499 if(command == "get-query-ring") {
3ddb9247 500 typedef pair<DNSName,uint16_t> query_t;
c89f8cd0 501 vector<query_t> queries;
509196af 502 bool filter=!req->getvars["public-filtered"].empty();
3ddb9247 503
c89f8cd0 504 if(req->getvars["name"]=="servfail-queries")
505 queries=broadcastAccFunction<vector<query_t> >(pleaseGetServfailQueryRing);
5208e0bf 506 else if(req->getvars["name"]=="bogus-queries")
66f2e6ad 507 queries=broadcastAccFunction<vector<query_t> >(pleaseGetBogusQueryRing);
c89f8cd0 508 else if(req->getvars["name"]=="queries")
509 queries=broadcastAccFunction<vector<query_t> >(pleaseGetQueryRing);
3ddb9247 510
c89f8cd0 511 typedef map<query_t,unsigned int> counts_t;
512 counts_t counts;
513 unsigned int total=0;
ef7cd021 514 for(const query_t& q : queries) {
c89f8cd0 515 total++;
509196af 516 if(filter)
3ddb9247
PD
517 counts[make_pair(getRegisteredName(q.first), q.second)]++;
518 else
519 counts[make_pair(q.first, q.second)]++;
c89f8cd0 520 }
3ddb9247 521
c89f8cd0 522 typedef std::multimap<int, query_t> rcounts_t;
523 rcounts_t rcounts;
3ddb9247 524
c89f8cd0 525 for(counts_t::const_iterator i=counts.begin(); i != counts.end(); ++i)
526 rcounts.insert(make_pair(-i->second, i->first));
527
e0e741df 528 Json::array entries;
509196af 529 unsigned int tot=0, totIncluded=0;
ef7cd021 530 for(const rcounts_t::value_type& q : rcounts) {
509196af 531 totIncluded-=q.first;
e0e741df 532 entries.push_back(Json::array {
d3ca14b2 533 -q.first, q.second.first.toLogString(), DNSRecordContent::NumberToType(q.second.second)
e0e741df 534 });
c89f8cd0 535 if(tot++>=100)
536 break;
537 }
509196af 538 if(queries.size() != totIncluded) {
e0e741df
CH
539 entries.push_back(Json::array {
540 (int)(queries.size() - totIncluded), "", ""
541 });
509196af 542 }
e0e741df 543 resp->setBody(Json::object { { "entries", entries } });
509196af 544 return;
545 }
546 else if(command == "get-remote-ring") {
547 vector<ComboAddress> queries;
548 if(req->getvars["name"]=="remotes")
549 queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetRemotes);
550 else if(req->getvars["name"]=="servfail-remotes")
551 queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetServfailRemotes);
66f2e6ad
KM
552 else if(req->getvars["name"]=="bogus-remotes")
553 queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetBogusRemotes);
509196af 554 else if(req->getvars["name"]=="large-answer-remotes")
555 queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetLargeAnswerRemotes);
621ccf89 556 else if(req->getvars["name"]=="timeouts")
557 queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetTimeouts);
3ddb9247 558
509196af 559 typedef map<ComboAddress,unsigned int,ComboAddress::addressOnlyLessThan> counts_t;
560 counts_t counts;
561 unsigned int total=0;
ef7cd021 562 for(const ComboAddress& q : queries) {
509196af 563 total++;
564 counts[q]++;
565 }
3ddb9247 566
509196af 567 typedef std::multimap<int, ComboAddress> rcounts_t;
568 rcounts_t rcounts;
3ddb9247 569
509196af 570 for(counts_t::const_iterator i=counts.begin(); i != counts.end(); ++i)
571 rcounts.insert(make_pair(-i->second, i->first));
572
e0e741df 573 Json::array entries;
509196af 574 unsigned int tot=0, totIncluded=0;
ef7cd021 575 for(const rcounts_t::value_type& q : rcounts) {
509196af 576 totIncluded-=q.first;
e0e741df
CH
577 entries.push_back(Json::array {
578 -q.first, q.second.toString()
579 });
509196af 580 if(tot++>=100)
581 break;
582 }
583 if(queries.size() != totIncluded) {
e0e741df 584 entries.push_back(Json::array {
d6dcfe36 585 (int)(queries.size() - totIncluded), ""
e0e741df 586 });
509196af 587 }
588
e0e741df 589 resp->setBody(Json::object { { "entries", entries } });
c89f8cd0 590 return;
3ae143b0 591 } else {
692829aa 592 resp->setErrorResult("Command '"+command+"' not found", 404);
3ae143b0 593 }
a7a902fb 594}
825fa717
CH
595
596
597void AsyncServerNewConnectionMT(void *p) {
598 AsyncServer *server = (AsyncServer*)p;
2cfc20bd 599
825fa717 600 try {
2cfc20bd 601 auto socket = server->accept(); // this is actually a shared_ptr
8a781bb5
RG
602 if (socket) {
603 server->d_asyncNewConnectionCallback(socket);
604 }
825fa717
CH
605 } catch (NetworkError &e) {
606 // we're running in a shared process/thread, so can't just terminate/abort.
e6a9dde5 607 g_log<<Logger::Warning<<"Network error in web thread: "<<e.what()<<endl;
825fa717
CH
608 return;
609 }
2cfc20bd 610 catch (...) {
e6a9dde5 611 g_log<<Logger::Warning<<"Unknown error in web thread"<<endl;
2cfc20bd 612
613 return;
614 }
615
825fa717
CH
616}
617
618void AsyncServer::asyncWaitForConnections(FDMultiplexer* fdm, const newconnectioncb_t& callback)
619{
620 d_asyncNewConnectionCallback = callback;
621 fdm->addReadFD(d_server_socket.getHandle(), boost::bind(&AsyncServer::newConnection, this));
622}
623
624void AsyncServer::newConnection()
625{
f165a1f4 626 getMT()->makeThread(&AsyncServerNewConnectionMT, this);
825fa717
CH
627}
628
5d7bd100 629// This is an entry point from FDM, so it needs to catch everything.
b030ea34 630void AsyncWebServer::serveConnection(std::shared_ptr<Socket> client) const {
f024c7bc 631 const string logprefix = d_logprefix + to_string(getUniqueID()) + " ";
b030ea34
PL
632
633 HttpRequest req(logprefix);
634 HttpResponse resp;
635 ComboAddress remote;
636 string reply;
637
825fa717 638 try {
b030ea34
PL
639 YaHTTP::AsyncRequestLoader yarl;
640 yarl.initialize(&req);
641 client->setNonBlocking();
642
643 string data;
644 try {
645 while(!req.complete) {
646 int bytes = arecvtcp(data, 16384, client.get(), true);
647 if (bytes > 0) {
648 req.complete = yarl.feed(data);
649 } else {
650 // read error OR EOF
651 break;
652 }
825fa717 653 }
b030ea34
PL
654 yarl.finalize();
655 } catch (YaHTTP::ParseError &e) {
656 // request stays incomplete
9fc86908 657 g_log<<Logger::Warning<<logprefix<<"Unable to parse request: "<<e.what()<<endl;
b030ea34
PL
658 }
659
660 if (d_loglevel >= WebServer::LogLevel::None) {
661 client->getRemote(remote);
825fa717 662 }
b030ea34 663
612ad9ec 664 logRequest(req, remote);
b030ea34
PL
665
666 WebServer::handleRequest(req, resp);
667 ostringstream ss;
668 resp.write(ss);
669 reply = ss.str();
670
612ad9ec 671 logResponse(resp, remote, logprefix);
b030ea34
PL
672
673 // now send the reply
674 if (asendtcp(reply, client.get()) == -1 || reply.empty()) {
675 g_log<<Logger::Error<<logprefix<<"Failed sending reply to HTTP client"<<endl;
676 }
677 }
678 catch(PDNSException &e) {
679 g_log<<Logger::Error<<logprefix<<"Exception: "<<e.reason<<endl;
680 }
681 catch(std::exception &e) {
682 if(strstr(e.what(), "timeout")==0)
683 g_log<<Logger::Error<<logprefix<<"STL Exception: "<<e.what()<<endl;
684 }
685 catch(...) {
686 g_log<<Logger::Error<<logprefix<<"Unknown exception"<<endl;
825fa717
CH
687 }
688
b030ea34
PL
689 if (d_loglevel >= WebServer::LogLevel::Normal) {
690 g_log<<Logger::Notice<<logprefix<<remote<<" \""<<req.method<<" "<<req.url.path<<" HTTP/"<<req.versionStr(req.version)<<"\" "<<resp.status<<" "<<reply.size()<<endl;
825fa717
CH
691 }
692}
693
694void AsyncWebServer::go() {
695 if (!d_server)
696 return;
690984d4
RG
697 auto server = std::dynamic_pointer_cast<AsyncServer>(d_server);
698 if (!server)
699 return;
700 server->asyncWaitForConnections(d_fdm, boost::bind(&AsyncWebServer::serveConnection, this, _1));
825fa717 701}