]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/dnsdist-web.cc
Merge pull request #4301 from Habbie/lua-ds-tostring
[thirdparty/pdns.git] / pdns / dnsdist-web.cc
CommitLineData
50bed881 1#include "dnsdist.hh"
2#include "sstuff.hh"
3#include "ext/json11/json11.hpp"
4#include "ext/incbin/incbin.h"
5#include "dolog.hh"
6#include <thread>
7#include <sstream>
8#include <yahttp/yahttp.hpp>
9#include "namespaces.hh"
10#include <sys/time.h>
11#include <sys/resource.h>
12#include "ext/incbin/incbin.h"
13#include "htmlfiles.h"
14#include "base64.hh"
85c7ca75 15#include "gettime.hh"
50bed881 16
50bed881 17
87893e08 18static bool compareAuthorization(YaHTTP::Request& req, const string &expected_password, const string& expectedApiKey)
50bed881 19{
20 // validate password
21 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
22 bool auth_ok = false;
23 if (header != req.headers.end() && toLower(header->second).find("basic ") == 0) {
24 string cookie = header->second.substr(6);
25
26 string plain;
27 B64Decode(cookie, plain);
28
29 vector<string> cparts;
30 stringtok(cparts, plain, ":");
31
32 // this gets rid of terminating zeros
33 auth_ok = (cparts.size()==2 && (0==strcmp(cparts[1].c_str(), expected_password.c_str())));
34 }
87893e08
RG
35 if (!auth_ok && !expectedApiKey.empty()) {
36 /* if this is a request for the API,
37 check if the API key is correct */
38 if (req.url.path=="/jsonstat" ||
c8adc34c
RG
39 req.url.path=="/api/v1/servers/localhost" ||
40 req.url.path=="/api/v1/servers/localhost/config" ||
41 req.url.path=="/api/v1/servers/localhost/statistics") {
87893e08
RG
42 header = req.headers.find("x-api-key");
43 if (header != req.headers.end()) {
44 auth_ok = (0==strcmp(header->second.c_str(), expectedApiKey.c_str()));
45 }
46 }
47 }
50bed881 48 return auth_ok;
49}
50
5387cc78
RG
51static void handleCORS(YaHTTP::Request& req, YaHTTP::Response& resp)
52{
53 YaHTTP::strstr_map_t::iterator origin = req.headers.find("Origin");
54 if (origin != req.headers.end()) {
55 if (req.method == "OPTIONS") {
56 /* Pre-flight request */
57 resp.headers["Access-Control-Allow-Methods"] = "GET";
87893e08 58 resp.headers["Access-Control-Allow-Headers"] = "Authorization, X-API-Key";
5387cc78
RG
59 }
60
61 resp.headers["Access-Control-Allow-Origin"] = origin->second;
62 resp.headers["Access-Control-Allow-Credentials"] = "true";
63 }
64}
50bed881 65
002decab
RG
66static void addSecurityHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
67{
68 static const std::vector<std::pair<std::string, std::string> > headers = {
69 { "X-Content-Type-Options", "nosniff" },
70 { "X-Frame-Options", "deny" },
71 { "X-Permitted-Cross-Domain-Policies", "none" },
72 { "X-XSS-Protection", "1; mode=block" },
73 { "Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'" },
74 };
75
76 for (const auto& h : headers) {
77 if (customHeaders) {
78 const auto& custom = customHeaders->find(h.first);
79 if (custom != customHeaders->end()) {
80 continue;
81 }
82 }
83 resp.headers[h.first] = h.second;
84 }
85}
86
87static void addCustomHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
88{
89 if (!customHeaders)
90 return;
91
92 for (const auto& c : *customHeaders) {
93 if (!c.second.empty()) {
94 resp.headers[c.first] = c.second;
95 }
96 }
97}
98
99static void connectionThread(int sock, ComboAddress remote, string password, string apiKey, const boost::optional<std::map<std::string, std::string> >& customHeaders)
50bed881 100{
101 using namespace json11;
fb7f8ec3 102 vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
438a189b 103 FILE* fp=0;
104 fp=fdopen(sock, "r");
105 try {
106 string line;
107 string request;
108 while(stringfgets(fp, line)) {
109 request+=line;
110 trim(line);
111
112 if(line.empty())
113 break;
114 }
50bed881 115
438a189b 116 std::istringstream ifs(request);
117 YaHTTP::Request req;
118 ifs >> req;
50bed881 119
438a189b 120 string command=req.getvars["command"];
50bed881 121
438a189b 122 req.getvars.erase("_"); // jQuery cache buster
50bed881 123
e3d76be2
RG
124 YaHTTP::Response resp;
125 resp.version = req.version;
c8c1a4fc 126 const string charset = "; charset=utf-8";
002decab
RG
127
128 addCustomHeaders(resp, customHeaders);
129 addSecurityHeaders(resp, customHeaders);
130
87893e08
RG
131 /* no need to send back the API key if any */
132 resp.headers.erase("X-API-Key");
50bed881 133
5387cc78
RG
134 if(req.method == "OPTIONS") {
135 /* the OPTIONS method should not require auth, otherwise it breaks CORS */
136 handleCORS(req, resp);
137 resp.status=200;
138 }
87893e08 139 else if (!compareAuthorization(req, password, apiKey)) {
cc55a8f2
JB
140 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
141 if (header != req.headers.end())
142 errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, remote.toStringWithPort());
438a189b 143 resp.status=401;
144 resp.body="<h1>Unauthorized</h1>";
145 resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
50bed881 146
50bed881 147 }
5387cc78
RG
148 else if(req.method != "GET") {
149 resp.status=405;
150 }
151 else if(req.url.path=="/jsonstat") {
152 handleCORS(req, resp);
438a189b 153 resp.status=200;
d745cd6f 154
5387cc78
RG
155 if(command=="stats") {
156 auto obj=Json::object {
157 { "packetcache-hits", 0},
158 { "packetcache-misses", 0},
159 { "over-capacity-drops", 0 },
160 { "too-old-drops", 0 },
161 { "server-policy", g_policy.getLocal()->name}
162 };
163
164 for(const auto& e : g_stats.entries) {
165 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&e.second))
ca12836d 166 obj.insert({e.first, (double)(*val)->load()});
5387cc78
RG
167 else if (const auto& val = boost::get<double*>(&e.second))
168 obj.insert({e.first, (**val)});
169 else
170 obj.insert({e.first, (int)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
171 }
172 Json my_json = obj;
173 resp.body=my_json.dump();
5d1c98df 174 resp.headers["Content-Type"] = "application/json";
d745cd6f 175 }
5387cc78
RG
176 else if(command=="dynblocklist") {
177 Json::object obj;
178 auto slow = g_dynblockNMG.getCopy();
179 struct timespec now;
85c7ca75 180 gettime(&now);
5387cc78
RG
181 for(const auto& e: slow) {
182 if(now < e->second.until ) {
183 Json::object thing{{"reason", e->second.reason}, {"seconds", (double)(e->second.until.tv_sec - now.tv_sec)},
78ffa782 184 {"blocks", (double)e->second.blocks} };
5387cc78
RG
185 obj.insert({e->first.toString(), thing});
186 }
187 }
0b71b874 188
189 auto slow2 = g_dynblockSMT.getCopy();
190 slow2.visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
191 if(now <node.d_value.until) {
192 string dom("empty");
193 if(!node.d_value.domain.empty())
194 dom = node.d_value.domain.toString();
195 Json::object thing{{"reason", node.d_value.reason}, {"seconds", (double)(node.d_value.until.tv_sec - now.tv_sec)},
196 {"blocks", (double)node.d_value.blocks} };
197
198 obj.insert({dom, thing});
199 }
200 });
201
202
203
5387cc78
RG
204 Json my_json = obj;
205 resp.body=my_json.dump();
5d1c98df 206 resp.headers["Content-Type"] = "application/json";
5387cc78
RG
207 }
208 else {
209 resp.status=404;
7be71139 210 }
7be71139 211 }
46d06a12 212 else if(req.url.path=="/api/v1/servers/localhost") {
5387cc78 213 handleCORS(req, resp);
438a189b 214 resp.status=200;
215
216 Json::array servers;
217 auto localServers = g_dstates.getCopy();
218 int num=0;
219 for(const auto& a : localServers) {
220 string status;
221 if(a->availability == DownstreamState::Availability::Up)
222 status = "UP";
223 else if(a->availability == DownstreamState::Availability::Down)
224 status = "DOWN";
225 else
226 status = (a->upStatus ? "up" : "down");
357c22dd 227 Json::array pools;
438a189b 228 for(const auto& p: a->pools)
357c22dd
RG
229 pools.push_back(p);
230
438a189b 231 Json::object server{
357c22dd 232 {"id", num++},
18eeccc9 233 {"name", a->name},
357c22dd
RG
234 {"address", a->remote.toStringWithPort()},
235 {"state", status},
236 {"qps", (int)a->queryLoad},
237 {"qpsLimit", (int)a->qps.getRate()},
238 {"outstanding", (int)a->outstanding},
239 {"reuseds", (int)a->reuseds},
240 {"weight", (int)a->weight},
241 {"order", (int)a->order},
242 {"pools", pools},
243 {"latency", (int)(a->latencyUsec/1000.0)},
244 {"queries", (int)a->queries}};
245
438a189b 246 servers.push_back(server);
247 }
248
65dec2a3
RG
249 Json::array frontends;
250 num=0;
251 for(const auto& front : g_frontends) {
252 if (front->udpFD == -1 && front->tcpFD == -1)
253 continue;
254 Json::object frontend{
255 { "id", num++ },
256 { "address", front->local.toStringWithPort() },
257 { "udp", front->udpFD >= 0 },
258 { "tcp", front->tcpFD >= 0 },
259 { "queries", (double) front->queries.load() }
260 };
261 frontends.push_back(frontend);
262 }
263
438a189b 264 Json::array rules;
265 auto localRules = g_rulactions.getCopy();
266 num=0;
267 for(const auto& a : localRules) {
268 Json::object rule{
269 {"id", num++},
1f4059be 270 {"matches", (int)a.first->d_matches},
271 {"rule", a.first->toString()},
272 {"action", a.second->toString()},
273 {"action-stats", a.second->getStats()}
274 };
438a189b 275 rules.push_back(rule);
276 }
da5d4999 277
278
279 string acl;
280
281 vector<string> vec;
fcadd56e 282
da5d4999 283 g_ACL.getCopy().toStringVector(&vec);
284
285 for(const auto& s : vec) {
286 if(!acl.empty()) acl += ", ";
287 acl+=s;
288 }
289 string localaddresses;
290 for(const auto& loc : g_locals) {
291 if(!localaddresses.empty()) localaddresses += ", ";
4c34246d 292 localaddresses += std::get<0>(loc).toStringWithPort();
da5d4999 293 }
50bed881 294
438a189b 295 Json my_json = Json::object {
296 { "daemon_type", "dnsdist" },
52f40d27 297 { "version", VERSION},
438a189b 298 { "servers", servers},
65dec2a3 299 { "frontends", frontends },
438a189b 300 { "rules", rules},
da5d4999 301 { "acl", acl},
302 { "local", localaddresses}
438a189b 303 };
5d1c98df 304 resp.headers["Content-Type"] = "application/json";
438a189b 305 resp.body=my_json.dump();
c8adc34c
RG
306 }
307 else if(req.url.path=="/api/v1/servers/localhost/statistics") {
308 handleCORS(req, resp);
309 resp.status=200;
310
311 Json::array doc;
312 for(const auto& item : g_stats.entries) {
313 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
314 doc.push_back(Json::object {
315 { "type", "StatisticItem" },
316 { "name", item.first },
317 { "value", (double)(*val)->load() }
318 });
319 }
320 else if (const auto& val = boost::get<double*>(&item.second)) {
321 doc.push_back(Json::object {
322 { "type", "StatisticItem" },
323 { "name", item.first },
324 { "value", (**val) }
325 });
326 }
327 else {
328 doc.push_back(Json::object {
329 { "type", "StatisticItem" },
330 { "name", item.first },
331 { "value", (int)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
332 });
333 }
334 }
335 Json my_json = doc;
336 resp.body=my_json.dump();
337 resp.headers["Content-Type"] = "application/json";
338 }
339 else if(req.url.path=="/api/v1/servers/localhost/config") {
340 handleCORS(req, resp);
341 resp.status=200;
50bed881 342
c8adc34c
RG
343 Json::array doc;
344 typedef boost::variant<bool, double, std::string> configentry_t;
345 std::vector<std::pair<std::string, configentry_t> > configEntries {
346 { "acl", g_ACL.getCopy().toString() },
347 { "control-socket", g_serverControl.toStringWithPort() },
348 { "ecs-override", g_ECSOverride },
349 { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
350 { "ecs-source-prefix-v6", (double) g_ECSSourcePrefixV6 },
351 { "fixup-case", g_fixupCase },
352 { "max-outstanding", (double) g_maxOutstanding },
353 { "server-policy", g_policy.getLocal()->name },
354 { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
355 { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
356 { "tcp-send-timeout", (double) g_tcpSendTimeout },
357 { "truncate-tc", g_truncateTC },
358 { "verbose", g_verbose },
359 { "verbose-health-checks", g_verboseHealthChecks }
360 };
361 for(const auto& item : configEntries) {
362 if (const auto& val = boost::get<bool>(&item.second)) {
363 doc.push_back(Json::object {
364 { "type", "ConfigSetting" },
365 { "name", item.first },
366 { "value", *val }
367 });
368 }
369 else if (const auto& val = boost::get<string>(&item.second)) {
370 doc.push_back(Json::object {
371 { "type", "ConfigSetting" },
372 { "name", item.first },
373 { "value", *val }
374 });
375 }
376 else if (const auto& val = boost::get<double>(&item.second)) {
377 doc.push_back(Json::object {
378 { "type", "ConfigSetting" },
379 { "name", item.first },
380 { "value", *val }
381 });
382 }
383 }
384 Json my_json = doc;
385 resp.body=my_json.dump();
386 resp.headers["Content-Type"] = "application/json";
438a189b 387 }
e3d76be2
RG
388 else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
389 resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
fe20a1c1 390 vector<string> parts;
e3d76be2 391 stringtok(parts, req.url.path, ".");
fe20a1c1 392 if(parts.back() == "html")
c8c1a4fc 393 resp.headers["Content-Type"] = "text/html" + charset;
fe20a1c1 394 else if(parts.back() == "css")
c8c1a4fc 395 resp.headers["Content-Type"] = "text/css" + charset;
fe20a1c1 396 else if(parts.back() == "js")
c8c1a4fc 397 resp.headers["Content-Type"] = "application/javascript" + charset;
864a6641
RG
398 else if(parts.back() == "png")
399 resp.headers["Content-Type"] = "image/png";
438a189b 400 resp.status=200;
401 }
e3d76be2 402 else if(req.url.path=="/") {
438a189b 403 resp.body.assign(g_urlmap["index.html"]);
c8c1a4fc 404 resp.headers["Content-Type"] = "text/html" + charset;
438a189b 405 resp.status=200;
406 }
407 else {
e3d76be2 408 // cerr<<"404 for: "<<req.url.path<<endl;
438a189b 409 resp.status=404;
410 }
50bed881 411
438a189b 412 std::ostringstream ofs;
413 ofs << resp;
414 string done;
415 done=ofs.str();
416 writen2(sock, done.c_str(), done.size());
50bed881 417
438a189b 418 fclose(fp);
419 fp=0;
420 }
59e9504e
RG
421 catch(const YaHTTP::ParseError& e) {
422 vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
423 if(fp)
424 fclose(fp);
425 }
34a6dd76 426 catch(const std::exception& e) {
59e9504e 427 errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
34a6dd76
RG
428 if(fp)
429 fclose(fp);
430 }
431 catch(...) {
59e9504e 432 errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
34a6dd76
RG
433 if(fp)
434 fclose(fp);
435 }
50bed881 436}
002decab 437void 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 438{
d745cd6f 439 warnlog("Webserver launched on %s", local.toStringWithPort());
50bed881 440 for(;;) {
441 try {
442 ComboAddress remote(local);
443 int fd = SAccept(sock, remote);
444 vinfolog("Got connection from %s", remote.toStringWithPort());
002decab 445 std::thread t(connectionThread, fd, remote, password, apiKey, customHeaders);
50bed881 446 t.detach();
447 }
448 catch(std::exception& e) {
449 errlog("Had an error accepting new webserver connection: %s", e.what());
450 }
451 }
452}