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