]>
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 | } | |
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 | 420 | 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 | 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 | } |