]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/dnsdist-web.cc
auth: switch circleci mssql image
[thirdparty/pdns.git] / pdns / dnsdist-web.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22 #include "dnsdist.hh"
23 #include "sstuff.hh"
24 #include "ext/json11/json11.hpp"
25 #include "ext/incbin/incbin.h"
26 #include "dolog.hh"
27 #include <thread>
28 #include "threadname.hh"
29 #include <sstream>
30 #include <yahttp/yahttp.hpp>
31 #include "namespaces.hh"
32 #include <sys/time.h>
33 #include <sys/resource.h>
34 #include "ext/incbin/incbin.h"
35 #include "htmlfiles.h"
36 #include "base64.hh"
37 #include "gettime.hh"
38 #include <boost/format.hpp>
39
40 bool g_apiReadWrite{false};
41 WebserverConfig g_webserverConfig;
42 std::string g_apiConfigDirectory;
43
44 static bool apiWriteConfigFile(const string& filebasename, const string& content)
45 {
46 if (!g_apiReadWrite) {
47 errlog("Not writing content to %s since the API is read-only", filebasename);
48 return false;
49 }
50
51 if (g_apiConfigDirectory.empty()) {
52 vinfolog("Not writing content to %s since the API configuration directory is not set", filebasename);
53 return false;
54 }
55
56 string filename = g_apiConfigDirectory + "/" + filebasename + ".conf";
57 ofstream ofconf(filename.c_str());
58 if (!ofconf) {
59 errlog("Could not open configuration fragment file '%s' for writing: %s", filename, stringerror());
60 return false;
61 }
62 ofconf << "-- Generated by the REST API, DO NOT EDIT" << endl;
63 ofconf << content << endl;
64 ofconf.close();
65 return true;
66 }
67
68 static void apiSaveACL(const NetmaskGroup& nmg)
69 {
70 vector<string> vec;
71 nmg.toStringVector(&vec);
72
73 string acl;
74 for(const auto& s : vec) {
75 if (!acl.empty()) {
76 acl += ", ";
77 }
78 acl += "\"" + s + "\"";
79 }
80
81 string content = "setACL({" + acl + "})";
82 apiWriteConfigFile("acl", content);
83 }
84
85 static bool checkAPIKey(const YaHTTP::Request& req, const string& expectedApiKey)
86 {
87 if (expectedApiKey.empty()) {
88 return false;
89 }
90
91 const auto header = req.headers.find("x-api-key");
92 if (header != req.headers.end()) {
93 return (header->second == expectedApiKey);
94 }
95
96 return false;
97 }
98
99 static bool checkWebPassword(const YaHTTP::Request& req, const string &expected_password)
100 {
101 static const char basicStr[] = "basic ";
102
103 const auto header = req.headers.find("authorization");
104
105 if (header != req.headers.end() && toLower(header->second).find(basicStr) == 0) {
106 string cookie = header->second.substr(sizeof(basicStr) - 1);
107
108 string plain;
109 B64Decode(cookie, plain);
110
111 vector<string> cparts;
112 stringtok(cparts, plain, ":");
113
114 if (cparts.size() == 2) {
115 return cparts[1] == expected_password;
116 }
117 }
118
119 return false;
120 }
121
122 static bool isAnAPIRequest(const YaHTTP::Request& req)
123 {
124 return req.url.path.find("/api/") == 0;
125 }
126
127 static bool isAnAPIRequestAllowedWithWebAuth(const YaHTTP::Request& req)
128 {
129 return req.url.path == "/api/v1/servers/localhost";
130 }
131
132 static bool isAStatsRequest(const YaHTTP::Request& req)
133 {
134 return req.url.path == "/jsonstat" || req.url.path == "/metrics";
135 }
136
137 static bool compareAuthorization(const YaHTTP::Request& req)
138 {
139 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
140
141 if (isAnAPIRequest(req)) {
142 /* Access to the API requires a valid API key */
143 if (checkAPIKey(req, g_webserverConfig.apiKey)) {
144 return true;
145 }
146
147 return isAnAPIRequestAllowedWithWebAuth(req) && checkWebPassword(req, g_webserverConfig.password);
148 }
149
150 if (isAStatsRequest(req)) {
151 /* Access to the stats is allowed for both API and Web users */
152 return checkAPIKey(req, g_webserverConfig.apiKey) || checkWebPassword(req, g_webserverConfig.password);
153 }
154
155 return checkWebPassword(req, g_webserverConfig.password);
156 }
157
158 static bool isMethodAllowed(const YaHTTP::Request& req)
159 {
160 if (req.method == "GET") {
161 return true;
162 }
163 if (req.method == "PUT" && g_apiReadWrite) {
164 if (req.url.path == "/api/v1/servers/localhost/config/allow-from") {
165 return true;
166 }
167 }
168 return false;
169 }
170
171 static void handleCORS(const YaHTTP::Request& req, YaHTTP::Response& resp)
172 {
173 const auto origin = req.headers.find("Origin");
174 if (origin != req.headers.end()) {
175 if (req.method == "OPTIONS") {
176 /* Pre-flight request */
177 if (g_apiReadWrite) {
178 resp.headers["Access-Control-Allow-Methods"] = "GET, PUT";
179 }
180 else {
181 resp.headers["Access-Control-Allow-Methods"] = "GET";
182 }
183 resp.headers["Access-Control-Allow-Headers"] = "Authorization, X-API-Key";
184 }
185
186 resp.headers["Access-Control-Allow-Origin"] = origin->second;
187
188 if (isAStatsRequest(req) || isAnAPIRequestAllowedWithWebAuth(req)) {
189 resp.headers["Access-Control-Allow-Credentials"] = "true";
190 }
191 }
192 }
193
194 static void addSecurityHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
195 {
196 static const std::vector<std::pair<std::string, std::string> > headers = {
197 { "X-Content-Type-Options", "nosniff" },
198 { "X-Frame-Options", "deny" },
199 { "X-Permitted-Cross-Domain-Policies", "none" },
200 { "X-XSS-Protection", "1; mode=block" },
201 { "Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'" },
202 };
203
204 for (const auto& h : headers) {
205 if (customHeaders) {
206 const auto& custom = customHeaders->find(h.first);
207 if (custom != customHeaders->end()) {
208 continue;
209 }
210 }
211 resp.headers[h.first] = h.second;
212 }
213 }
214
215 static void addCustomHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
216 {
217 if (!customHeaders)
218 return;
219
220 for (const auto& c : *customHeaders) {
221 if (!c.second.empty()) {
222 resp.headers[c.first] = c.second;
223 }
224 }
225 }
226
227 template<typename T>
228 static json11::Json::array someResponseRulesToJson(GlobalStateHolder<vector<T>>* someResponseRules)
229 {
230 using namespace json11;
231 Json::array responseRules;
232 int num=0;
233 auto localResponseRules = someResponseRules->getLocal();
234 for(const auto& a : *localResponseRules) {
235 Json::object rule{
236 {"id", num++},
237 {"creationOrder", (double)a.d_creationOrder},
238 {"uuid", boost::uuids::to_string(a.d_id)},
239 {"matches", (double)a.d_rule->d_matches},
240 {"rule", a.d_rule->toString()},
241 {"action", a.d_action->toString()},
242 };
243 responseRules.push_back(rule);
244 }
245 return responseRules;
246 }
247
248 static void connectionThread(int sock, ComboAddress remote)
249 {
250 setThreadName("dnsdist/webConn");
251
252 using namespace json11;
253 vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
254
255 try {
256 YaHTTP::AsyncRequestLoader yarl;
257 YaHTTP::Request req;
258 bool finished = false;
259
260 yarl.initialize(&req);
261 while(!finished) {
262 int bytes;
263 char buf[1024];
264 bytes = read(sock, buf, sizeof(buf));
265 if (bytes > 0) {
266 string data = string(buf, bytes);
267 finished = yarl.feed(data);
268 } else {
269 // read error OR EOF
270 break;
271 }
272 }
273 yarl.finalize();
274
275 string command=req.getvars["command"];
276
277 req.getvars.erase("_"); // jQuery cache buster
278
279 YaHTTP::Response resp;
280 resp.version = req.version;
281 const string charset = "; charset=utf-8";
282
283 {
284 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
285
286 addCustomHeaders(resp, g_webserverConfig.customHeaders);
287 addSecurityHeaders(resp, g_webserverConfig.customHeaders);
288 }
289 /* indicate that the connection will be closed after completion of the response */
290 resp.headers["Connection"] = "close";
291
292 /* no need to send back the API key if any */
293 resp.headers.erase("X-API-Key");
294
295 if(req.method == "OPTIONS") {
296 /* the OPTIONS method should not require auth, otherwise it breaks CORS */
297 handleCORS(req, resp);
298 resp.status=200;
299 }
300 else if (!compareAuthorization(req)) {
301 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
302 if (header != req.headers.end())
303 errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, remote.toStringWithPort());
304 resp.status=401;
305 resp.body="<h1>Unauthorized</h1>";
306 resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
307
308 }
309 else if(!isMethodAllowed(req)) {
310 resp.status=405;
311 }
312 else if(req.url.path=="/jsonstat") {
313 handleCORS(req, resp);
314 resp.status=200;
315
316 if(command=="stats") {
317 auto obj=Json::object {
318 { "packetcache-hits", 0},
319 { "packetcache-misses", 0},
320 { "over-capacity-drops", 0 },
321 { "too-old-drops", 0 },
322 { "server-policy", g_policy.getLocal()->name}
323 };
324
325 for(const auto& e : g_stats.entries) {
326 if (e.first == "special-memory-usage")
327 continue; // Too expensive for get-all
328 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&e.second))
329 obj.insert({e.first, (double)(*val)->load()});
330 else if (const auto& dval = boost::get<double*>(&e.second))
331 obj.insert({e.first, (**dval)});
332 else
333 obj.insert({e.first, (double)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
334 }
335 Json my_json = obj;
336 resp.body=my_json.dump();
337 resp.headers["Content-Type"] = "application/json";
338 }
339 else if(command=="dynblocklist") {
340 Json::object obj;
341 auto nmg = g_dynblockNMG.getLocal();
342 struct timespec now;
343 gettime(&now);
344 for(const auto& e: *nmg) {
345 if(now < e->second.until ) {
346 Json::object thing{
347 {"reason", e->second.reason},
348 {"seconds", (double)(e->second.until.tv_sec - now.tv_sec)},
349 {"blocks", (double)e->second.blocks},
350 {"action", DNSAction::typeToString(e->second.action != DNSAction::Action::None ? e->second.action : g_dynBlockAction) },
351 {"warning", e->second.warning }
352 };
353 obj.insert({e->first.toString(), thing});
354 }
355 }
356
357 auto smt = g_dynblockSMT.getLocal();
358 smt->visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
359 if(now <node.d_value.until) {
360 string dom("empty");
361 if(!node.d_value.domain.empty())
362 dom = node.d_value.domain.toString();
363 Json::object thing{
364 {"reason", node.d_value.reason},
365 {"seconds", (double)(node.d_value.until.tv_sec - now.tv_sec)},
366 {"blocks", (double)node.d_value.blocks},
367 {"action", DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction) }
368 };
369 obj.insert({dom, thing});
370 }
371 });
372
373
374
375 Json my_json = obj;
376 resp.body=my_json.dump();
377 resp.headers["Content-Type"] = "application/json";
378 }
379 else if(command=="ebpfblocklist") {
380 Json::object obj;
381 #ifdef HAVE_EBPF
382 struct timespec now;
383 gettime(&now);
384 for (const auto& dynbpf : g_dynBPFFilters) {
385 std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > addrStats = dynbpf->getAddrStats();
386 for (const auto& entry : addrStats) {
387 Json::object thing
388 {
389 {"seconds", (double)(std::get<2>(entry).tv_sec - now.tv_sec)},
390 {"blocks", (double)(std::get<1>(entry))}
391 };
392 obj.insert({std::get<0>(entry).toString(), thing });
393 }
394 }
395 #endif /* HAVE_EBPF */
396 Json my_json = obj;
397 resp.body=my_json.dump();
398 resp.headers["Content-Type"] = "application/json";
399 }
400 else {
401 resp.status=404;
402 }
403 }
404 else if (req.url.path == "/metrics") {
405 handleCORS(req, resp);
406 resp.status = 200;
407
408 std::ostringstream output;
409 for (const auto& e : g_stats.entries) {
410 if (e.first == "special-memory-usage")
411 continue; // Too expensive for get-all
412 std::string metricName = std::get<0>(e);
413
414 // Prometheus suggest using '_' instead of '-'
415 std::string prometheusMetricName = "dnsdist_" + boost::replace_all_copy(metricName, "-", "_");
416
417 MetricDefinition metricDetails;
418
419 if (!g_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
420 vinfolog("Do not have metric details for %s", metricName);
421 continue;
422 }
423
424 std::string prometheusTypeName = g_metricDefinitions.getPrometheusStringMetricType(metricDetails.prometheusType);
425
426 if (prometheusTypeName == "") {
427 vinfolog("Unknown Prometheus type for %s", metricName);
428 continue;
429 }
430
431 // for these we have the help and types encoded in the sources:
432 output << "# HELP " << prometheusMetricName << " " << metricDetails.description << "\n";
433 output << "# TYPE " << prometheusMetricName << " " << prometheusTypeName << "\n";
434 output << prometheusMetricName << " ";
435
436 if (const auto& val = boost::get<DNSDistStats::stat_t*>(&std::get<1>(e)))
437 output << (*val)->load();
438 else if (const auto& dval = boost::get<double*>(&std::get<1>(e)))
439 output << **dval;
440 else
441 output << (*boost::get<DNSDistStats::statfunction_t>(&std::get<1>(e)))(std::get<0>(e));
442
443 output << "\n";
444 }
445
446 // Latency histogram buckets
447 output << "# HELP dnsdist_latency Histogram of responses by latency\n";
448 output << "# TYPE dnsdist_latency histogram\n";
449 uint64_t latency_amounts = g_stats.latency0_1;
450 output << "dnsdist_latency_bucket{le=\"1\"} " << latency_amounts << "\n";
451 latency_amounts += g_stats.latency1_10;
452 output << "dnsdist_latency_bucket{le=\"10\"} " << latency_amounts << "\n";
453 latency_amounts += g_stats.latency10_50;
454 output << "dnsdist_latency_bucket{le=\"50\"} " << latency_amounts << "\n";
455 latency_amounts += g_stats.latency50_100;
456 output << "dnsdist_latency_bucket{le=\"100\"} " << latency_amounts << "\n";
457 latency_amounts += g_stats.latency100_1000;
458 output << "dnsdist_latency_bucket{le=\"1000\"} " << latency_amounts << "\n";
459 latency_amounts += g_stats.latencySlow; // Should be the same as latency_count
460 output << "dnsdist_latency_bucket{le=\"+Inf\"} " << latency_amounts << "\n";
461
462 auto states = g_dstates.getLocal();
463 const string statesbase = "dnsdist_server_";
464
465 output << "# HELP " << statesbase << "queries " << "Amount of queries relayed to server" << "\n";
466 output << "# TYPE " << statesbase << "queries " << "counter" << "\n";
467 output << "# HELP " << statesbase << "drops " << "Amount of queries not answered by server" << "\n";
468 output << "# TYPE " << statesbase << "drops " << "counter" << "\n";
469 output << "# HELP " << statesbase << "latency " << "Server's latency when answering questions in miliseconds" << "\n";
470 output << "# TYPE " << statesbase << "latency " << "gauge" << "\n";
471 output << "# HELP " << statesbase << "senderrors " << "Total number of OS snd errors while relaying queries" << "\n";
472 output << "# TYPE " << statesbase << "senderrors " << "counter" << "\n";
473 output << "# HELP " << statesbase << "outstanding " << "Current number of queries that are waiting for a backend response" << "\n";
474 output << "# TYPE " << statesbase << "outstanding " << "gauge" << "\n";
475 output << "# HELP " << statesbase << "order " << "The order in which this server is picked" << "\n";
476 output << "# TYPE " << statesbase << "order " << "gauge" << "\n";
477 output << "# HELP " << statesbase << "weight " << "The weight within the order in which this server is picked" << "\n";
478 output << "# TYPE " << statesbase << "weight " << "gauge" << "\n";
479 output << "# HELP " << statesbase << "tcpdiedsendingquery " << "The number of TCP I/O errors while sending the query" << "\n";
480 output << "# TYPE " << statesbase << "tcpdiedsendingquery " << "counter" << "\n";
481 output << "# HELP " << statesbase << "tcpdiedreadingresponse " << "The number of TCP I/O errors while reading the response" << "\n";
482 output << "# TYPE " << statesbase << "tcpdiedreadingresponse " << "counter" << "\n";
483 output << "# HELP " << statesbase << "tcpgaveup " << "The number of TCP connections failing after too many attempts" << "\n";
484 output << "# TYPE " << statesbase << "tcpgaveup " << "counter" << "\n";
485 output << "# HELP " << statesbase << "tcpreadtimeouts " << "The number of TCP read timeouts" << "\n";
486 output << "# TYPE " << statesbase << "tcpreadtimeouts " << "counter" << "\n";
487 output << "# HELP " << statesbase << "tcpwritetimeouts " << "The number of TCP write timeouts" << "\n";
488 output << "# TYPE " << statesbase << "tcpwritetimeouts " << "counter" << "\n";
489 output << "# HELP " << statesbase << "tcpcurrentconnections " << "The number of current TCP connections" << "\n";
490 output << "# TYPE " << statesbase << "tcpcurrentconnections " << "gauge" << "\n";
491 output << "# HELP " << statesbase << "tcpavgqueriesperconn " << "The average number of queries per TCP connection" << "\n";
492 output << "# TYPE " << statesbase << "tcpavgqueriesperconn " << "gauge" << "\n";
493 output << "# HELP " << statesbase << "tcpavgconnduration " << "The average duration of a TCP connection (ms)" << "\n";
494 output << "# TYPE " << statesbase << "tcpavgconnduration " << "gauge" << "\n";
495
496 for (const auto& state : *states) {
497 string serverName;
498
499 if (state->name.empty())
500 serverName = state->remote.toStringWithPort();
501 else
502 serverName = state->getName();
503
504 boost::replace_all(serverName, ".", "_");
505
506 const std::string label = boost::str(boost::format("{server=\"%1%\",address=\"%2%\"}")
507 % serverName % state->remote.toStringWithPort());
508
509 output << statesbase << "queries" << label << " " << state->queries.load() << "\n";
510 output << statesbase << "drops" << label << " " << state->reuseds.load() << "\n";
511 output << statesbase << "latency" << label << " " << state->latencyUsec/1000.0 << "\n";
512 output << statesbase << "senderrors" << label << " " << state->sendErrors.load() << "\n";
513 output << statesbase << "outstanding" << label << " " << state->outstanding.load() << "\n";
514 output << statesbase << "order" << label << " " << state->order << "\n";
515 output << statesbase << "weight" << label << " " << state->weight << "\n";
516 output << statesbase << "tcpdiedsendingquery" << label << " " << state->tcpDiedSendingQuery << "\n";
517 output << statesbase << "tcpdiedreadingresponse" << label << " " << state->tcpDiedReadingResponse << "\n";
518 output << statesbase << "tcpgaveup" << label << " " << state->tcpGaveUp << "\n";
519 output << statesbase << "tcpreadtimeouts" << label << " " << state->tcpReadTimeouts << "\n";
520 output << statesbase << "tcpwritetimeouts" << label << " " << state->tcpWriteTimeouts << "\n";
521 output << statesbase << "tcpcurrentconnections" << label << " " << state->tcpCurrentConnections << "\n";
522 output << statesbase << "tcpavgqueriesperconn" << label << " " << state->tcpAvgQueriesPerConnection << "\n";
523 output << statesbase << "tcpavgconnduration" << label << " " << state->tcpAvgConnectionDuration << "\n";
524 }
525
526 for (const auto& front : g_frontends) {
527 if (front->udpFD == -1 && front->tcpFD == -1)
528 continue;
529
530 string frontName = front->local.toString() + ":" + std::to_string(front->local.getPort());
531 string proto = (front->udpFD >= 0 ? "udp" : "tcp");
532
533 output << "dnsdist_frontend_queries{frontend=\"" << frontName << "\",proto=\"" << proto
534 << "\"} " << front->queries.load() << "\n";
535 }
536
537 auto localPools = g_pools.getLocal();
538 const string cachebase = "dnsdist_pool_";
539
540 for (const auto& entry : *localPools) {
541 string poolName = entry.first;
542
543 if (poolName.empty()) {
544 poolName = "_default_";
545 }
546 const string label = "{pool=\"" + poolName + "\"}";
547 const std::shared_ptr<ServerPool> pool = entry.second;
548 output << "dnsdist_pool_servers" << label << " " << pool->countServers(false) << "\n";
549 output << "dnsdist_pool_active_servers" << label << " " << pool->countServers(true) << "\n";
550
551 if (pool->packetCache != nullptr) {
552 const auto& cache = pool->packetCache;
553
554 output << cachebase << "cache_size" <<label << " " << cache->getMaxEntries() << "\n";
555 output << cachebase << "cache_entries" <<label << " " << cache->getEntriesCount() << "\n";
556 output << cachebase << "cache_hits" <<label << " " << cache->getHits() << "\n";
557 output << cachebase << "cache_misses" <<label << " " << cache->getMisses() << "\n";
558 output << cachebase << "cache_deferred_inserts" <<label << " " << cache->getDeferredInserts() << "\n";
559 output << cachebase << "cache_deferred_lookups" <<label << " " << cache->getDeferredLookups() << "\n";
560 output << cachebase << "cache_lookup_collisions" <<label << " " << cache->getLookupCollisions() << "\n";
561 output << cachebase << "cache_insert_collisions" <<label << " " << cache->getInsertCollisions() << "\n";
562 output << cachebase << "cache_ttl_too_shorts" <<label << " " << cache->getTTLTooShorts() << "\n";
563 }
564 }
565
566 resp.body = output.str();
567 resp.headers["Content-Type"] = "text/plain";
568 }
569
570 else if(req.url.path=="/api/v1/servers/localhost") {
571 handleCORS(req, resp);
572 resp.status=200;
573
574 Json::array servers;
575 auto localServers = g_dstates.getLocal();
576 int num=0;
577 for(const auto& a : *localServers) {
578 string status;
579 if(a->availability == DownstreamState::Availability::Up)
580 status = "UP";
581 else if(a->availability == DownstreamState::Availability::Down)
582 status = "DOWN";
583 else
584 status = (a->upStatus ? "up" : "down");
585
586 Json::array pools;
587 for(const auto& p: a->pools)
588 pools.push_back(p);
589
590 Json::object server{
591 {"id", num++},
592 {"name", a->name},
593 {"address", a->remote.toStringWithPort()},
594 {"state", status},
595 {"qps", (double)a->queryLoad},
596 {"qpsLimit", (double)a->qps.getRate()},
597 {"outstanding", (double)a->outstanding},
598 {"reuseds", (double)a->reuseds},
599 {"weight", (double)a->weight},
600 {"order", (double)a->order},
601 {"pools", pools},
602 {"latency", (double)(a->latencyUsec/1000.0)},
603 {"queries", (double)a->queries},
604 {"sendErrors", (double)a->sendErrors},
605 {"tcpDiedSendingQuery", (double)a->tcpDiedSendingQuery},
606 {"tcpDiedReadingResponse", (double)a->tcpDiedReadingResponse},
607 {"tcpGaveUp", (double)a->tcpGaveUp},
608 {"tcpReadTimeouts", (double)a->tcpReadTimeouts},
609 {"tcpWriteTimeouts", (double)a->tcpWriteTimeouts},
610 {"tcpCurrentConnections", (double)a->tcpCurrentConnections},
611 {"tcpAvgQueriesPerConnection", (double)a->tcpAvgQueriesPerConnection},
612 {"tcpAvgConnectionDuration", (double)a->tcpAvgConnectionDuration},
613 {"dropRate", (double)a->dropRate}
614 };
615
616 /* sending a latency for a DOWN server doesn't make sense */
617 if (a->availability == DownstreamState::Availability::Down) {
618 server["latency"] = nullptr;
619 }
620
621 servers.push_back(server);
622 }
623
624 Json::array frontends;
625 num=0;
626 for(const auto& front : g_frontends) {
627 if (front->udpFD == -1 && front->tcpFD == -1)
628 continue;
629 Json::object frontend{
630 { "id", num++ },
631 { "address", front->local.toStringWithPort() },
632 { "udp", front->udpFD >= 0 },
633 { "tcp", front->tcpFD >= 0 },
634 { "type", front->getType() },
635 { "queries", (double) front->queries.load() },
636 { "tcpDiedReadingQuery", (double) front->tcpDiedReadingQuery.load() },
637 { "tcpDiedSendingResponse", (double) front->tcpDiedSendingResponse.load() },
638 { "tcpGaveUp", (double) front->tcpGaveUp.load() },
639 { "tcpClientTimeouts", (double) front->tcpClientTimeouts },
640 { "tcpDownstreamTimeouts", (double) front->tcpDownstreamTimeouts },
641 { "tcpCurrentConnections", (double) front->tcpCurrentConnections },
642 { "tcpAvgQueriesPerConnection", (double) front->tcpAvgQueriesPerConnection },
643 { "tcpAvgConnectionDuration", (double) front->tcpAvgConnectionDuration },
644 };
645 frontends.push_back(frontend);
646 }
647
648 Json::array pools;
649 auto localPools = g_pools.getLocal();
650 num=0;
651 for(const auto& pool : *localPools) {
652 const auto& cache = pool.second->packetCache;
653 Json::object entry {
654 { "id", num++ },
655 { "name", pool.first },
656 { "serversCount", (double) pool.second->countServers(false) },
657 { "cacheSize", (double) (cache ? cache->getMaxEntries() : 0) },
658 { "cacheEntries", (double) (cache ? cache->getEntriesCount() : 0) },
659 { "cacheHits", (double) (cache ? cache->getHits() : 0) },
660 { "cacheMisses", (double) (cache ? cache->getMisses() : 0) },
661 { "cacheDeferredInserts", (double) (cache ? cache->getDeferredInserts() : 0) },
662 { "cacheDeferredLookups", (double) (cache ? cache->getDeferredLookups() : 0) },
663 { "cacheLookupCollisions", (double) (cache ? cache->getLookupCollisions() : 0) },
664 { "cacheInsertCollisions", (double) (cache ? cache->getInsertCollisions() : 0) },
665 { "cacheTTLTooShorts", (double) (cache ? cache->getTTLTooShorts() : 0) }
666 };
667 pools.push_back(entry);
668 }
669
670 Json::array rules;
671 auto localRules = g_rulactions.getLocal();
672 num=0;
673 for(const auto& a : *localRules) {
674 Json::object rule{
675 {"id", num++},
676 {"creationOrder", (double)a.d_creationOrder},
677 {"uuid", boost::uuids::to_string(a.d_id)},
678 {"matches", (double)a.d_rule->d_matches},
679 {"rule", a.d_rule->toString()},
680 {"action", a.d_action->toString()},
681 {"action-stats", a.d_action->getStats()}
682 };
683 rules.push_back(rule);
684 }
685
686 auto responseRules = someResponseRulesToJson(&g_resprulactions);
687 auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitresprulactions);
688 auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredresprulactions);
689
690 string acl;
691
692 vector<string> vec;
693 g_ACL.getLocal()->toStringVector(&vec);
694
695 for(const auto& s : vec) {
696 if(!acl.empty()) acl += ", ";
697 acl+=s;
698 }
699 string localaddresses;
700 for(const auto& front : g_frontends) {
701 if (front->tcp) {
702 continue;
703 }
704 if (!localaddresses.empty()) {
705 localaddresses += ", ";
706 }
707 localaddresses += front->local.toStringWithPort();
708 }
709
710 Json my_json = Json::object {
711 { "daemon_type", "dnsdist" },
712 { "version", VERSION},
713 { "servers", servers},
714 { "frontends", frontends },
715 { "pools", pools },
716 { "rules", rules},
717 { "response-rules", responseRules},
718 { "cache-hit-response-rules", cacheHitResponseRules},
719 { "self-answered-response-rules", selfAnsweredResponseRules},
720 { "acl", acl},
721 { "local", localaddresses}
722 };
723 resp.headers["Content-Type"] = "application/json";
724 resp.body=my_json.dump();
725 }
726 else if(req.url.path=="/api/v1/servers/localhost/statistics") {
727 handleCORS(req, resp);
728 resp.status=200;
729
730 Json::array doc;
731 for(const auto& item : g_stats.entries) {
732 if (item.first == "special-memory-usage")
733 continue; // Too expensive for get-all
734
735 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
736 doc.push_back(Json::object {
737 { "type", "StatisticItem" },
738 { "name", item.first },
739 { "value", (double)(*val)->load() }
740 });
741 }
742 else if (const auto& dval = boost::get<double*>(&item.second)) {
743 doc.push_back(Json::object {
744 { "type", "StatisticItem" },
745 { "name", item.first },
746 { "value", (**dval) }
747 });
748 }
749 else {
750 doc.push_back(Json::object {
751 { "type", "StatisticItem" },
752 { "name", item.first },
753 { "value", (double)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
754 });
755 }
756 }
757 Json my_json = doc;
758 resp.body=my_json.dump();
759 resp.headers["Content-Type"] = "application/json";
760 }
761 else if(req.url.path=="/api/v1/servers/localhost/config") {
762 handleCORS(req, resp);
763 resp.status=200;
764
765 Json::array doc;
766 typedef boost::variant<bool, double, std::string> configentry_t;
767 std::vector<std::pair<std::string, configentry_t> > configEntries {
768 { "acl", g_ACL.getLocal()->toString() },
769 { "allow-empty-response", g_allowEmptyResponse },
770 { "control-socket", g_serverControl.toStringWithPort() },
771 { "ecs-override", g_ECSOverride },
772 { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
773 { "ecs-source-prefix-v6", (double) g_ECSSourcePrefixV6 },
774 { "fixup-case", g_fixupCase },
775 { "max-outstanding", (double) g_maxOutstanding },
776 { "server-policy", g_policy.getLocal()->name },
777 { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
778 { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
779 { "tcp-send-timeout", (double) g_tcpSendTimeout },
780 { "truncate-tc", g_truncateTC },
781 { "verbose", g_verbose },
782 { "verbose-health-checks", g_verboseHealthChecks }
783 };
784 for(const auto& item : configEntries) {
785 if (const auto& bval = boost::get<bool>(&item.second)) {
786 doc.push_back(Json::object {
787 { "type", "ConfigSetting" },
788 { "name", item.first },
789 { "value", *bval }
790 });
791 }
792 else if (const auto& sval = boost::get<string>(&item.second)) {
793 doc.push_back(Json::object {
794 { "type", "ConfigSetting" },
795 { "name", item.first },
796 { "value", *sval }
797 });
798 }
799 else if (const auto& dval = boost::get<double>(&item.second)) {
800 doc.push_back(Json::object {
801 { "type", "ConfigSetting" },
802 { "name", item.first },
803 { "value", *dval }
804 });
805 }
806 }
807 Json my_json = doc;
808 resp.body=my_json.dump();
809 resp.headers["Content-Type"] = "application/json";
810 }
811 else if(req.url.path=="/api/v1/servers/localhost/config/allow-from") {
812 handleCORS(req, resp);
813
814 resp.headers["Content-Type"] = "application/json";
815 resp.status=200;
816
817 if (req.method == "PUT") {
818 std::string err;
819 Json doc = Json::parse(req.body, err);
820
821 if (!doc.is_null()) {
822 NetmaskGroup nmg;
823 auto aclList = doc["value"];
824 if (aclList.is_array()) {
825
826 for (auto value : aclList.array_items()) {
827 try {
828 nmg.addMask(value.string_value());
829 } catch (NetmaskException &e) {
830 resp.status = 400;
831 break;
832 }
833 }
834
835 if (resp.status == 200) {
836 infolog("Updating the ACL via the API to %s", nmg.toString());
837 g_ACL.setState(nmg);
838 apiSaveACL(nmg);
839 }
840 }
841 else {
842 resp.status = 400;
843 }
844 }
845 else {
846 resp.status = 400;
847 }
848 }
849 if (resp.status == 200) {
850 Json::array acl;
851 vector<string> vec;
852 g_ACL.getLocal()->toStringVector(&vec);
853
854 for(const auto& s : vec) {
855 acl.push_back(s);
856 }
857
858 Json::object obj{
859 { "type", "ConfigSetting" },
860 { "name", "allow-from" },
861 { "value", acl }
862 };
863 Json my_json = obj;
864 resp.body=my_json.dump();
865 }
866 }
867 else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
868 resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
869 vector<string> parts;
870 stringtok(parts, req.url.path, ".");
871 if(parts.back() == "html")
872 resp.headers["Content-Type"] = "text/html" + charset;
873 else if(parts.back() == "css")
874 resp.headers["Content-Type"] = "text/css" + charset;
875 else if(parts.back() == "js")
876 resp.headers["Content-Type"] = "application/javascript" + charset;
877 else if(parts.back() == "png")
878 resp.headers["Content-Type"] = "image/png";
879 resp.status=200;
880 }
881 else if(req.url.path=="/") {
882 resp.body.assign(g_urlmap["index.html"]);
883 resp.headers["Content-Type"] = "text/html" + charset;
884 resp.status=200;
885 }
886 else {
887 // cerr<<"404 for: "<<req.url.path<<endl;
888 resp.status=404;
889 }
890
891 std::ostringstream ofs;
892 ofs << resp;
893 string done;
894 done=ofs.str();
895 writen2(sock, done.c_str(), done.size());
896
897 close(sock);
898 sock = -1;
899 }
900 catch(const YaHTTP::ParseError& e) {
901 vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
902 close(sock);
903 }
904 catch(const std::exception& e) {
905 errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
906 close(sock);
907 }
908 catch(...) {
909 errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
910 close(sock);
911 }
912 }
913
914 void setWebserverAPIKey(const boost::optional<std::string> apiKey)
915 {
916 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
917
918 if (apiKey) {
919 g_webserverConfig.apiKey = *apiKey;
920 } else {
921 g_webserverConfig.apiKey.clear();
922 }
923 }
924
925 void setWebserverPassword(const std::string& password)
926 {
927 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
928
929 g_webserverConfig.password = password;
930 }
931
932 void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders)
933 {
934 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
935
936 g_webserverConfig.customHeaders = customHeaders;
937 }
938
939 void dnsdistWebserverThread(int sock, const ComboAddress& local)
940 {
941 setThreadName("dnsdist/webserv");
942 warnlog("Webserver launched on %s", local.toStringWithPort());
943 for(;;) {
944 try {
945 ComboAddress remote(local);
946 int fd = SAccept(sock, remote);
947 vinfolog("Got connection from %s", remote.toStringWithPort());
948 std::thread t(connectionThread, fd, remote);
949 t.detach();
950 }
951 catch(std::exception& e) {
952 errlog("Had an error accepting new webserver connection: %s", e.what());
953 }
954 }
955 }