]>
Commit | Line | Data |
---|---|---|
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 | 18 | static 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 |
51 | static 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 |
66 | static 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 | ||
87 | static 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 | ||
99 | static 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 | 437 | void 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 | } |