]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/dnsdist-web.cc
dnsdist: Send a latency of 0 over carbon, null over API for down servers
[thirdparty/pdns.git] / pdns / dnsdist-web.cc
CommitLineData
12471842
PL
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 */
50bed881 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 <sstream>
29#include <yahttp/yahttp.hpp>
30#include "namespaces.hh"
31#include <sys/time.h>
32#include <sys/resource.h>
33#include "ext/incbin/incbin.h"
34#include "htmlfiles.h"
35#include "base64.hh"
85c7ca75 36#include "gettime.hh"
50bed881 37
56d68fad
RG
38bool g_apiReadWrite{false};
39std::string g_apiConfigDirectory;
40
41static bool apiWriteConfigFile(const string& filebasename, const string& content)
42{
43 if (!g_apiReadWrite) {
44 errlog("Not writing content to %s since the API is read-only", filebasename);
45 return false;
46 }
47
48 if (g_apiConfigDirectory.empty()) {
49 vinfolog("Not writing content to %s since the API configuration directory is not set", filebasename);
50 return false;
51 }
52
53 string filename = g_apiConfigDirectory + "/" + filebasename + ".conf";
54 ofstream ofconf(filename.c_str());
55 if (!ofconf) {
56 errlog("Could not open configuration fragment file '%s' for writing: %s", filename, stringerror());
57 return false;
58 }
59 ofconf << "-- Generated by the REST API, DO NOT EDIT" << endl;
60 ofconf << content << endl;
61 ofconf.close();
62 return true;
63}
64
65static void apiSaveACL(const NetmaskGroup& nmg)
66{
67 vector<string> vec;
68 g_ACL.getCopy().toStringVector(&vec);
69
70 string acl;
71 for(const auto& s : vec) {
72 if (!acl.empty()) {
73 acl += ", ";
74 }
75 acl += "\"" + s + "\"";
76 }
77
78 string content = "setACL({" + acl + "})";
79 apiWriteConfigFile("acl", content);
80}
50bed881 81
87893e08 82static bool compareAuthorization(YaHTTP::Request& req, const string &expected_password, const string& expectedApiKey)
50bed881 83{
84 // validate password
85 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
86 bool auth_ok = false;
87 if (header != req.headers.end() && toLower(header->second).find("basic ") == 0) {
88 string cookie = header->second.substr(6);
89
90 string plain;
91 B64Decode(cookie, plain);
92
93 vector<string> cparts;
94 stringtok(cparts, plain, ":");
95
96 // this gets rid of terminating zeros
97 auth_ok = (cparts.size()==2 && (0==strcmp(cparts[1].c_str(), expected_password.c_str())));
98 }
87893e08
RG
99 if (!auth_ok && !expectedApiKey.empty()) {
100 /* if this is a request for the API,
101 check if the API key is correct */
102 if (req.url.path=="/jsonstat" ||
c8adc34c
RG
103 req.url.path=="/api/v1/servers/localhost" ||
104 req.url.path=="/api/v1/servers/localhost/config" ||
56d68fad 105 req.url.path=="/api/v1/servers/localhost/config/allow-from" ||
c8adc34c 106 req.url.path=="/api/v1/servers/localhost/statistics") {
87893e08
RG
107 header = req.headers.find("x-api-key");
108 if (header != req.headers.end()) {
109 auth_ok = (0==strcmp(header->second.c_str(), expectedApiKey.c_str()));
110 }
111 }
112 }
50bed881 113 return auth_ok;
114}
115
56d68fad
RG
116static bool isMethodAllowed(const YaHTTP::Request& req)
117{
118 if (req.method == "GET") {
119 return true;
120 }
121 if (req.method == "PUT" && g_apiReadWrite) {
122 if (req.url.path == "/api/v1/servers/localhost/config/allow-from") {
123 return true;
124 }
125 }
126 return false;
127}
128
5387cc78
RG
129static void handleCORS(YaHTTP::Request& req, YaHTTP::Response& resp)
130{
131 YaHTTP::strstr_map_t::iterator origin = req.headers.find("Origin");
132 if (origin != req.headers.end()) {
133 if (req.method == "OPTIONS") {
134 /* Pre-flight request */
56d68fad
RG
135 if (g_apiReadWrite) {
136 resp.headers["Access-Control-Allow-Methods"] = "GET, PUT";
137 }
138 else {
139 resp.headers["Access-Control-Allow-Methods"] = "GET";
140 }
87893e08 141 resp.headers["Access-Control-Allow-Headers"] = "Authorization, X-API-Key";
5387cc78
RG
142 }
143
144 resp.headers["Access-Control-Allow-Origin"] = origin->second;
145 resp.headers["Access-Control-Allow-Credentials"] = "true";
146 }
147}
50bed881 148
002decab
RG
149static void addSecurityHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
150{
151 static const std::vector<std::pair<std::string, std::string> > headers = {
152 { "X-Content-Type-Options", "nosniff" },
153 { "X-Frame-Options", "deny" },
154 { "X-Permitted-Cross-Domain-Policies", "none" },
155 { "X-XSS-Protection", "1; mode=block" },
156 { "Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'" },
157 };
158
159 for (const auto& h : headers) {
160 if (customHeaders) {
161 const auto& custom = customHeaders->find(h.first);
162 if (custom != customHeaders->end()) {
163 continue;
164 }
165 }
166 resp.headers[h.first] = h.second;
167 }
168}
169
170static void addCustomHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
171{
172 if (!customHeaders)
173 return;
174
175 for (const auto& c : *customHeaders) {
176 if (!c.second.empty()) {
177 resp.headers[c.first] = c.second;
178 }
179 }
180}
181
182static void connectionThread(int sock, ComboAddress remote, string password, string apiKey, const boost::optional<std::map<std::string, std::string> >& customHeaders)
50bed881 183{
184 using namespace json11;
fb7f8ec3 185 vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
56d68fad 186
438a189b 187 try {
56d68fad 188 YaHTTP::AsyncRequestLoader yarl;
438a189b 189 YaHTTP::Request req;
56d68fad
RG
190 bool finished = false;
191
192 yarl.initialize(&req);
193 while(!finished) {
194 int bytes;
195 char buf[1024];
196 bytes = read(sock, buf, sizeof(buf));
197 if (bytes > 0) {
198 string data = string(buf, bytes);
199 finished = yarl.feed(data);
200 } else {
201 // read error OR EOF
202 break;
203 }
204 }
205 yarl.finalize();
50bed881 206
438a189b 207 string command=req.getvars["command"];
50bed881 208
438a189b 209 req.getvars.erase("_"); // jQuery cache buster
50bed881 210
e3d76be2
RG
211 YaHTTP::Response resp;
212 resp.version = req.version;
c8c1a4fc 213 const string charset = "; charset=utf-8";
002decab
RG
214
215 addCustomHeaders(resp, customHeaders);
216 addSecurityHeaders(resp, customHeaders);
217
87893e08
RG
218 /* no need to send back the API key if any */
219 resp.headers.erase("X-API-Key");
50bed881 220
5387cc78
RG
221 if(req.method == "OPTIONS") {
222 /* the OPTIONS method should not require auth, otherwise it breaks CORS */
223 handleCORS(req, resp);
224 resp.status=200;
225 }
87893e08 226 else if (!compareAuthorization(req, password, apiKey)) {
cc55a8f2
JB
227 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
228 if (header != req.headers.end())
229 errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, remote.toStringWithPort());
438a189b 230 resp.status=401;
231 resp.body="<h1>Unauthorized</h1>";
232 resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
50bed881 233
50bed881 234 }
56d68fad 235 else if(!isMethodAllowed(req)) {
5387cc78
RG
236 resp.status=405;
237 }
238 else if(req.url.path=="/jsonstat") {
239 handleCORS(req, resp);
438a189b 240 resp.status=200;
d745cd6f 241
5387cc78
RG
242 if(command=="stats") {
243 auto obj=Json::object {
244 { "packetcache-hits", 0},
245 { "packetcache-misses", 0},
246 { "over-capacity-drops", 0 },
247 { "too-old-drops", 0 },
248 { "server-policy", g_policy.getLocal()->name}
249 };
250
251 for(const auto& e : g_stats.entries) {
252 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&e.second))
ca12836d 253 obj.insert({e.first, (double)(*val)->load()});
5387cc78
RG
254 else if (const auto& val = boost::get<double*>(&e.second))
255 obj.insert({e.first, (**val)});
256 else
257 obj.insert({e.first, (int)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
258 }
259 Json my_json = obj;
260 resp.body=my_json.dump();
5d1c98df 261 resp.headers["Content-Type"] = "application/json";
d745cd6f 262 }
5387cc78
RG
263 else if(command=="dynblocklist") {
264 Json::object obj;
265 auto slow = g_dynblockNMG.getCopy();
266 struct timespec now;
85c7ca75 267 gettime(&now);
5387cc78
RG
268 for(const auto& e: slow) {
269 if(now < e->second.until ) {
8429ad04
RG
270 Json::object thing{
271 {"reason", e->second.reason},
272 {"seconds", (double)(e->second.until.tv_sec - now.tv_sec)},
273 {"blocks", (double)e->second.blocks}
274 };
5387cc78
RG
275 obj.insert({e->first.toString(), thing});
276 }
277 }
0b71b874 278
279 auto slow2 = g_dynblockSMT.getCopy();
280 slow2.visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
281 if(now <node.d_value.until) {
282 string dom("empty");
283 if(!node.d_value.domain.empty())
284 dom = node.d_value.domain.toString();
285 Json::object thing{{"reason", node.d_value.reason}, {"seconds", (double)(node.d_value.until.tv_sec - now.tv_sec)},
286 {"blocks", (double)node.d_value.blocks} };
287
288 obj.insert({dom, thing});
289 }
290 });
291
292
293
8429ad04
RG
294 Json my_json = obj;
295 resp.body=my_json.dump();
296 resp.headers["Content-Type"] = "application/json";
297 }
298 else if(command=="ebpfblocklist") {
299 Json::object obj;
300#ifdef HAVE_EBPF
301 struct timespec now;
302 gettime(&now);
303 for (const auto& dynbpf : g_dynBPFFilters) {
304 std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > addrStats = dynbpf->getAddrStats();
305 for (const auto& entry : addrStats) {
306 Json::object thing
307 {
308 {"seconds", (double)(std::get<2>(entry).tv_sec - now.tv_sec)},
309 {"blocks", (double)(std::get<1>(entry))}
310 };
311 obj.insert({std::get<0>(entry).toString(), thing });
312 }
313 }
314#endif /* HAVE_EBPF */
5387cc78
RG
315 Json my_json = obj;
316 resp.body=my_json.dump();
5d1c98df 317 resp.headers["Content-Type"] = "application/json";
5387cc78
RG
318 }
319 else {
320 resp.status=404;
7be71139 321 }
7be71139 322 }
46d06a12 323 else if(req.url.path=="/api/v1/servers/localhost") {
5387cc78 324 handleCORS(req, resp);
438a189b 325 resp.status=200;
326
327 Json::array servers;
328 auto localServers = g_dstates.getCopy();
329 int num=0;
330 for(const auto& a : localServers) {
331 string status;
332 if(a->availability == DownstreamState::Availability::Up)
333 status = "UP";
334 else if(a->availability == DownstreamState::Availability::Down)
335 status = "DOWN";
336 else
337 status = (a->upStatus ? "up" : "down");
357c22dd 338 Json::array pools;
438a189b 339 for(const auto& p: a->pools)
357c22dd
RG
340 pools.push_back(p);
341
438a189b 342 Json::object server{
357c22dd 343 {"id", num++},
18eeccc9 344 {"name", a->name},
357c22dd
RG
345 {"address", a->remote.toStringWithPort()},
346 {"state", status},
347 {"qps", (int)a->queryLoad},
348 {"qpsLimit", (int)a->qps.getRate()},
349 {"outstanding", (int)a->outstanding},
350 {"reuseds", (int)a->reuseds},
351 {"weight", (int)a->weight},
352 {"order", (int)a->order},
353 {"pools", pools},
354 {"latency", (int)(a->latencyUsec/1000.0)},
355 {"queries", (int)a->queries}};
356
36927800
RG
357 /* sending a latency for a DOWN server doesn't make sense */
358 if (a->availability == DownstreamState::Availability::Down) {
359 server["latency"] = nullptr;
360 }
361
438a189b 362 servers.push_back(server);
363 }
364
65dec2a3
RG
365 Json::array frontends;
366 num=0;
367 for(const auto& front : g_frontends) {
368 if (front->udpFD == -1 && front->tcpFD == -1)
369 continue;
370 Json::object frontend{
371 { "id", num++ },
372 { "address", front->local.toStringWithPort() },
373 { "udp", front->udpFD >= 0 },
374 { "tcp", front->tcpFD >= 0 },
375 { "queries", (double) front->queries.load() }
376 };
377 frontends.push_back(frontend);
378 }
379
438a189b 380 Json::array rules;
381 auto localRules = g_rulactions.getCopy();
382 num=0;
383 for(const auto& a : localRules) {
384 Json::object rule{
385 {"id", num++},
1f4059be 386 {"matches", (int)a.first->d_matches},
387 {"rule", a.first->toString()},
388 {"action", a.second->toString()},
389 {"action-stats", a.second->getStats()}
390 };
438a189b 391 rules.push_back(rule);
392 }
da5d4999 393
46e8b49e
RG
394 Json::array responseRules;
395 auto localResponseRules = g_resprulactions.getCopy();
396 num=0;
397 for(const auto& a : localResponseRules) {
398 Json::object rule{
399 {"id", num++},
400 {"matches", (int)a.first->d_matches},
401 {"rule", a.first->toString()},
402 {"action", a.second->toString()},
403 };
404 responseRules.push_back(rule);
405 }
da5d4999 406
407 string acl;
408
409 vector<string> vec;
da5d4999 410 g_ACL.getCopy().toStringVector(&vec);
411
412 for(const auto& s : vec) {
413 if(!acl.empty()) acl += ", ";
414 acl+=s;
415 }
416 string localaddresses;
417 for(const auto& loc : g_locals) {
418 if(!localaddresses.empty()) localaddresses += ", ";
4c34246d 419 localaddresses += std::get<0>(loc).toStringWithPort();
da5d4999 420 }
50bed881 421
438a189b 422 Json my_json = Json::object {
423 { "daemon_type", "dnsdist" },
52f40d27 424 { "version", VERSION},
438a189b 425 { "servers", servers},
65dec2a3 426 { "frontends", frontends },
438a189b 427 { "rules", rules},
46e8b49e 428 { "response-rules", responseRules},
da5d4999 429 { "acl", acl},
430 { "local", localaddresses}
438a189b 431 };
5d1c98df 432 resp.headers["Content-Type"] = "application/json";
438a189b 433 resp.body=my_json.dump();
c8adc34c
RG
434 }
435 else if(req.url.path=="/api/v1/servers/localhost/statistics") {
436 handleCORS(req, resp);
437 resp.status=200;
438
439 Json::array doc;
440 for(const auto& item : g_stats.entries) {
441 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
442 doc.push_back(Json::object {
443 { "type", "StatisticItem" },
444 { "name", item.first },
445 { "value", (double)(*val)->load() }
446 });
447 }
448 else if (const auto& val = boost::get<double*>(&item.second)) {
449 doc.push_back(Json::object {
450 { "type", "StatisticItem" },
451 { "name", item.first },
452 { "value", (**val) }
453 });
454 }
455 else {
456 doc.push_back(Json::object {
457 { "type", "StatisticItem" },
458 { "name", item.first },
459 { "value", (int)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
460 });
461 }
462 }
463 Json my_json = doc;
464 resp.body=my_json.dump();
465 resp.headers["Content-Type"] = "application/json";
466 }
467 else if(req.url.path=="/api/v1/servers/localhost/config") {
468 handleCORS(req, resp);
469 resp.status=200;
50bed881 470
c8adc34c
RG
471 Json::array doc;
472 typedef boost::variant<bool, double, std::string> configentry_t;
473 std::vector<std::pair<std::string, configentry_t> > configEntries {
474 { "acl", g_ACL.getCopy().toString() },
475 { "control-socket", g_serverControl.toStringWithPort() },
476 { "ecs-override", g_ECSOverride },
477 { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
478 { "ecs-source-prefix-v6", (double) g_ECSSourcePrefixV6 },
479 { "fixup-case", g_fixupCase },
480 { "max-outstanding", (double) g_maxOutstanding },
481 { "server-policy", g_policy.getLocal()->name },
482 { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
483 { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
484 { "tcp-send-timeout", (double) g_tcpSendTimeout },
485 { "truncate-tc", g_truncateTC },
486 { "verbose", g_verbose },
487 { "verbose-health-checks", g_verboseHealthChecks }
488 };
489 for(const auto& item : configEntries) {
490 if (const auto& val = boost::get<bool>(&item.second)) {
491 doc.push_back(Json::object {
492 { "type", "ConfigSetting" },
493 { "name", item.first },
494 { "value", *val }
495 });
496 }
497 else if (const auto& val = boost::get<string>(&item.second)) {
498 doc.push_back(Json::object {
499 { "type", "ConfigSetting" },
500 { "name", item.first },
501 { "value", *val }
502 });
503 }
504 else if (const auto& val = boost::get<double>(&item.second)) {
505 doc.push_back(Json::object {
506 { "type", "ConfigSetting" },
507 { "name", item.first },
508 { "value", *val }
509 });
510 }
511 }
512 Json my_json = doc;
513 resp.body=my_json.dump();
514 resp.headers["Content-Type"] = "application/json";
438a189b 515 }
56d68fad
RG
516 else if(req.url.path=="/api/v1/servers/localhost/config/allow-from") {
517 handleCORS(req, resp);
518
519 resp.headers["Content-Type"] = "application/json";
520 resp.status=200;
521
522 if (req.method == "PUT") {
523 std::string err;
524 Json doc = Json::parse(req.body, err);
525
526 if (!doc.is_null()) {
527 NetmaskGroup nmg;
528 auto aclList = doc["value"];
529 if (aclList.is_array()) {
530
531 for (auto value : aclList.array_items()) {
532 try {
533 nmg.addMask(value.string_value());
534 } catch (NetmaskException &e) {
535 resp.status = 400;
536 break;
537 }
538 }
539
540 if (resp.status == 200) {
541 infolog("Updating the ACL via the API to %s", nmg.toString());
542 g_ACL.setState(nmg);
543 apiSaveACL(nmg);
544 }
545 }
546 else {
547 resp.status = 400;
548 }
549 }
550 else {
551 resp.status = 400;
552 }
553 }
554 if (resp.status == 200) {
555 Json::array acl;
556 vector<string> vec;
557 g_ACL.getCopy().toStringVector(&vec);
558
559 for(const auto& s : vec) {
560 acl.push_back(s);
561 }
562
563 Json::object obj{
564 { "type", "ConfigSetting" },
565 { "name", "allow-from" },
566 { "value", acl }
567 };
568 Json my_json = obj;
569 resp.body=my_json.dump();
570 }
571 }
e3d76be2
RG
572 else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
573 resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
fe20a1c1 574 vector<string> parts;
e3d76be2 575 stringtok(parts, req.url.path, ".");
fe20a1c1 576 if(parts.back() == "html")
c8c1a4fc 577 resp.headers["Content-Type"] = "text/html" + charset;
fe20a1c1 578 else if(parts.back() == "css")
c8c1a4fc 579 resp.headers["Content-Type"] = "text/css" + charset;
fe20a1c1 580 else if(parts.back() == "js")
c8c1a4fc 581 resp.headers["Content-Type"] = "application/javascript" + charset;
864a6641
RG
582 else if(parts.back() == "png")
583 resp.headers["Content-Type"] = "image/png";
438a189b 584 resp.status=200;
585 }
e3d76be2 586 else if(req.url.path=="/") {
438a189b 587 resp.body.assign(g_urlmap["index.html"]);
c8c1a4fc 588 resp.headers["Content-Type"] = "text/html" + charset;
438a189b 589 resp.status=200;
590 }
591 else {
e3d76be2 592 // cerr<<"404 for: "<<req.url.path<<endl;
438a189b 593 resp.status=404;
594 }
50bed881 595
438a189b 596 std::ostringstream ofs;
597 ofs << resp;
598 string done;
599 done=ofs.str();
600 writen2(sock, done.c_str(), done.size());
50bed881 601
56d68fad
RG
602 close(sock);
603 sock = -1;
438a189b 604 }
59e9504e
RG
605 catch(const YaHTTP::ParseError& e) {
606 vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
56d68fad 607 close(sock);
59e9504e 608 }
34a6dd76 609 catch(const std::exception& e) {
59e9504e 610 errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
56d68fad 611 close(sock);
34a6dd76
RG
612 }
613 catch(...) {
59e9504e 614 errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
56d68fad 615 close(sock);
34a6dd76 616 }
50bed881 617}
002decab 618void dnsdistWebserverThread(int sock, const ComboAddress& local, const std::string& password, const std::string& apiKey, const boost::optional<std::map<std::string, std::string> >& customHeaders)
50bed881 619{
d745cd6f 620 warnlog("Webserver launched on %s", local.toStringWithPort());
50bed881 621 for(;;) {
622 try {
623 ComboAddress remote(local);
624 int fd = SAccept(sock, remote);
625 vinfolog("Got connection from %s", remote.toStringWithPort());
002decab 626 std::thread t(connectionThread, fd, remote, password, apiKey, customHeaders);
50bed881 627 t.detach();
628 }
629 catch(std::exception& e) {
630 errlog("Had an error accepting new webserver connection: %s", e.what());
631 }
632 }
633}