]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/dnsdist-web.cc
links..
[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 }
188 Json my_json = obj;
189 resp.body=my_json.dump();
5d1c98df 190 resp.headers["Content-Type"] = "application/json";
5387cc78
RG
191 }
192 else {
193 resp.status=404;
7be71139 194 }
7be71139 195 }
46d06a12 196 else if(req.url.path=="/api/v1/servers/localhost") {
5387cc78 197 handleCORS(req, resp);
438a189b 198 resp.status=200;
199
200 Json::array servers;
201 auto localServers = g_dstates.getCopy();
202 int num=0;
203 for(const auto& a : localServers) {
204 string status;
205 if(a->availability == DownstreamState::Availability::Up)
206 status = "UP";
207 else if(a->availability == DownstreamState::Availability::Down)
208 status = "DOWN";
209 else
210 status = (a->upStatus ? "up" : "down");
211 string pools;
212 for(const auto& p: a->pools)
213 pools+=p+" ";
214 Json::object server{
215 {"id", num++},
18eeccc9 216 {"name", a->name},
438a189b 217 {"address", a->remote.toStringWithPort()},
218 {"state", status},
219 {"qps", (int)a->queryLoad},
220 {"qpsLimit", (int)a->qps.getRate()},
221 {"outstanding", (int)a->outstanding},
222 {"reuseds", (int)a->reuseds},
223 {"weight", (int)a->weight},
224 {"order", (int)a->order},
225 {"pools", pools},
f20fbb61 226 {"latency", (int)(a->latencyUsec/1000.0)},
438a189b 227 {"queries", (int)a->queries}};
228
229 servers.push_back(server);
230 }
231
65dec2a3
RG
232 Json::array frontends;
233 num=0;
234 for(const auto& front : g_frontends) {
235 if (front->udpFD == -1 && front->tcpFD == -1)
236 continue;
237 Json::object frontend{
238 { "id", num++ },
239 { "address", front->local.toStringWithPort() },
240 { "udp", front->udpFD >= 0 },
241 { "tcp", front->tcpFD >= 0 },
242 { "queries", (double) front->queries.load() }
243 };
244 frontends.push_back(frontend);
245 }
246
438a189b 247 Json::array rules;
248 auto localRules = g_rulactions.getCopy();
249 num=0;
250 for(const auto& a : localRules) {
251 Json::object rule{
252 {"id", num++},
1f4059be 253 {"matches", (int)a.first->d_matches},
254 {"rule", a.first->toString()},
255 {"action", a.second->toString()},
256 {"action-stats", a.second->getStats()}
257 };
438a189b 258 rules.push_back(rule);
259 }
da5d4999 260
261
262 string acl;
263
264 vector<string> vec;
fcadd56e 265
da5d4999 266 g_ACL.getCopy().toStringVector(&vec);
267
268 for(const auto& s : vec) {
269 if(!acl.empty()) acl += ", ";
270 acl+=s;
271 }
272 string localaddresses;
273 for(const auto& loc : g_locals) {
274 if(!localaddresses.empty()) localaddresses += ", ";
4c34246d 275 localaddresses += std::get<0>(loc).toStringWithPort();
da5d4999 276 }
50bed881 277
438a189b 278 Json my_json = Json::object {
279 { "daemon_type", "dnsdist" },
52f40d27 280 { "version", VERSION},
438a189b 281 { "servers", servers},
65dec2a3 282 { "frontends", frontends },
438a189b 283 { "rules", rules},
da5d4999 284 { "acl", acl},
285 { "local", localaddresses}
438a189b 286 };
5d1c98df 287 resp.headers["Content-Type"] = "application/json";
438a189b 288 resp.body=my_json.dump();
c8adc34c
RG
289 }
290 else if(req.url.path=="/api/v1/servers/localhost/statistics") {
291 handleCORS(req, resp);
292 resp.status=200;
293
294 Json::array doc;
295 for(const auto& item : g_stats.entries) {
296 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
297 doc.push_back(Json::object {
298 { "type", "StatisticItem" },
299 { "name", item.first },
300 { "value", (double)(*val)->load() }
301 });
302 }
303 else if (const auto& val = boost::get<double*>(&item.second)) {
304 doc.push_back(Json::object {
305 { "type", "StatisticItem" },
306 { "name", item.first },
307 { "value", (**val) }
308 });
309 }
310 else {
311 doc.push_back(Json::object {
312 { "type", "StatisticItem" },
313 { "name", item.first },
314 { "value", (int)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
315 });
316 }
317 }
318 Json my_json = doc;
319 resp.body=my_json.dump();
320 resp.headers["Content-Type"] = "application/json";
321 }
322 else if(req.url.path=="/api/v1/servers/localhost/config") {
323 handleCORS(req, resp);
324 resp.status=200;
50bed881 325
c8adc34c
RG
326 Json::array doc;
327 typedef boost::variant<bool, double, std::string> configentry_t;
328 std::vector<std::pair<std::string, configentry_t> > configEntries {
329 { "acl", g_ACL.getCopy().toString() },
330 { "control-socket", g_serverControl.toStringWithPort() },
331 { "ecs-override", g_ECSOverride },
332 { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
333 { "ecs-source-prefix-v6", (double) g_ECSSourcePrefixV6 },
334 { "fixup-case", g_fixupCase },
335 { "max-outstanding", (double) g_maxOutstanding },
336 { "server-policy", g_policy.getLocal()->name },
337 { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
338 { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
339 { "tcp-send-timeout", (double) g_tcpSendTimeout },
340 { "truncate-tc", g_truncateTC },
341 { "verbose", g_verbose },
342 { "verbose-health-checks", g_verboseHealthChecks }
343 };
344 for(const auto& item : configEntries) {
345 if (const auto& val = boost::get<bool>(&item.second)) {
346 doc.push_back(Json::object {
347 { "type", "ConfigSetting" },
348 { "name", item.first },
349 { "value", *val }
350 });
351 }
352 else if (const auto& val = boost::get<string>(&item.second)) {
353 doc.push_back(Json::object {
354 { "type", "ConfigSetting" },
355 { "name", item.first },
356 { "value", *val }
357 });
358 }
359 else if (const auto& val = boost::get<double>(&item.second)) {
360 doc.push_back(Json::object {
361 { "type", "ConfigSetting" },
362 { "name", item.first },
363 { "value", *val }
364 });
365 }
366 }
367 Json my_json = doc;
368 resp.body=my_json.dump();
369 resp.headers["Content-Type"] = "application/json";
438a189b 370 }
e3d76be2
RG
371 else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
372 resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
fe20a1c1 373 vector<string> parts;
e3d76be2 374 stringtok(parts, req.url.path, ".");
fe20a1c1 375 if(parts.back() == "html")
c8c1a4fc 376 resp.headers["Content-Type"] = "text/html" + charset;
fe20a1c1 377 else if(parts.back() == "css")
c8c1a4fc 378 resp.headers["Content-Type"] = "text/css" + charset;
fe20a1c1 379 else if(parts.back() == "js")
c8c1a4fc 380 resp.headers["Content-Type"] = "application/javascript" + charset;
864a6641
RG
381 else if(parts.back() == "png")
382 resp.headers["Content-Type"] = "image/png";
438a189b 383 resp.status=200;
384 }
e3d76be2 385 else if(req.url.path=="/") {
438a189b 386 resp.body.assign(g_urlmap["index.html"]);
c8c1a4fc 387 resp.headers["Content-Type"] = "text/html" + charset;
438a189b 388 resp.status=200;
389 }
390 else {
e3d76be2 391 // cerr<<"404 for: "<<req.url.path<<endl;
438a189b 392 resp.status=404;
393 }
50bed881 394
438a189b 395 std::ostringstream ofs;
396 ofs << resp;
397 string done;
398 done=ofs.str();
399 writen2(sock, done.c_str(), done.size());
50bed881 400
438a189b 401 fclose(fp);
402 fp=0;
403 }
59e9504e
RG
404 catch(const YaHTTP::ParseError& e) {
405 vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
406 if(fp)
407 fclose(fp);
408 }
34a6dd76 409 catch(const std::exception& e) {
59e9504e 410 errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
34a6dd76
RG
411 if(fp)
412 fclose(fp);
413 }
414 catch(...) {
59e9504e 415 errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
34a6dd76
RG
416 if(fp)
417 fclose(fp);
418 }
50bed881 419}
002decab 420void 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 421{
d745cd6f 422 warnlog("Webserver launched on %s", local.toStringWithPort());
50bed881 423 for(;;) {
424 try {
425 ComboAddress remote(local);
426 int fd = SAccept(sock, remote);
427 vinfolog("Got connection from %s", remote.toStringWithPort());
002decab 428 std::thread t(connectionThread, fd, remote, password, apiKey, customHeaders);
50bed881 429 t.detach();
430 }
431 catch(std::exception& e) {
432 errlog("Had an error accepting new webserver connection: %s", e.what());
433 }
434 }
435}