]>
Commit | Line | Data |
---|---|---|
a7a902fb | 1 | /* |
6edbf68a PL |
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 | */ | |
870a0fe4 AT |
22 | #ifdef HAVE_CONFIG_H |
23 | #include "config.h" | |
24 | #endif | |
2470b36e | 25 | #include "ws-recursor.hh" |
a7a902fb | 26 | #include "json.hh" |
fa8fd4d2 | 27 | |
a7a902fb BH |
28 | #include <string> |
29 | #include "namespaces.hh" | |
30 | #include <iostream> | |
31 | #include "iputils.hh" | |
32 | #include "rec_channel.hh" | |
33 | #include "arguments.hh" | |
34 | #include "misc.hh" | |
8465487d | 35 | #include "syncres.hh" |
c89f8cd0 | 36 | #include "dnsparser.hh" |
639a6287 | 37 | #include "json11.hpp" |
3ae143b0 | 38 | #include "webserver.hh" |
6ec5e728 | 39 | #include "ws-api.hh" |
41942bb3 | 40 | #include "logger.hh" |
d579380d | 41 | #include "ext/incbin/incbin.h" |
2278ffda PL |
42 | #include "rec-lua-conf.hh" |
43 | #include "rpzloader.hh" | |
b030ea34 | 44 | #include "uuid-utils.hh" |
fb4b38f1 | 45 | |
6dfff36f | 46 | extern thread_local FDMultiplexer* t_fdm; |
825fa717 | 47 | |
639a6287 | 48 | using json11::Json; |
a7a902fb | 49 | |
6ec5e728 CH |
50 | void productServerStatisticsFetch(map<string,string>& out) |
51 | { | |
72259676 | 52 | map<string,string> stats = getAllStatsMap(StatComponent::API); |
6ec5e728 CH |
53 | out.swap(stats); |
54 | } | |
55 | ||
5376a5d7 RG |
56 | boost::optional<uint64_t> productServerStatisticsFetch(const std::string& name) |
57 | { | |
58 | return getStatByName(name); | |
59 | } | |
60 | ||
c348c0c8 CH |
61 | static void apiWriteConfigFile(const string& filebasename, const string& content) |
62 | { | |
d07bf7ff PL |
63 | if (::arg()["api-config-dir"].empty()) { |
64 | throw ApiException("Config Option \"api-config-dir\" must be set"); | |
c348c0c8 CH |
65 | } |
66 | ||
d07bf7ff | 67 | string filename = ::arg()["api-config-dir"] + "/" + filebasename + ".conf"; |
c348c0c8 CH |
68 | ofstream ofconf(filename.c_str()); |
69 | if (!ofconf) { | |
70 | throw ApiException("Could not open config fragment file '"+filename+"' for writing: "+stringerror()); | |
71 | } | |
72 | ofconf << "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl; | |
73 | ofconf << content << endl; | |
74 | ofconf.close(); | |
75 | } | |
76 | ||
41942bb3 CH |
77 | static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp) |
78 | { | |
2054afbb | 79 | if (req->method == "PUT") { |
639a6287 | 80 | Json document = req->json(); |
41942bb3 | 81 | |
639a6287 CH |
82 | auto jlist = document["value"]; |
83 | if (!jlist.is_array()) { | |
bd0320fe | 84 | throw ApiException("'value' must be an array"); |
41942bb3 CH |
85 | } |
86 | ||
5f8108e9 | 87 | NetmaskGroup nmg; |
639a6287 | 88 | for (auto value : jlist.array_items()) { |
faa0f891 | 89 | try { |
5f8108e9 RG |
90 | nmg.addMask(value.string_value()); |
91 | } catch (const NetmaskException &e) { | |
faa0f891 CH |
92 | throw ApiException(e.reason); |
93 | } | |
94 | } | |
95 | ||
41942bb3 CH |
96 | ostringstream ss; |
97 | ||
98 | // Clear allow-from-file if set, so our changes take effect | |
99 | ss << "allow-from-file=" << endl; | |
100 | ||
101 | // Clear allow-from, and provide a "parent" value | |
102 | ss << "allow-from=" << endl; | |
5f8108e9 | 103 | ss << "allow-from+=" << nmg.toString() << endl; |
41942bb3 CH |
104 | |
105 | apiWriteConfigFile("allow-from", ss.str()); | |
106 | ||
107 | parseACLs(); | |
108 | ||
109 | // fall through to GET | |
110 | } else if (req->method != "GET") { | |
111 | throw HttpMethodNotAllowedException(); | |
112 | } | |
113 | ||
114 | // Return currently configured ACLs | |
41942bb3 CH |
115 | vector<string> entries; |
116 | t_allowFrom->toStringVector(&entries); | |
117 | ||
639a6287 CH |
118 | resp->setBody(Json::object { |
119 | { "name", "allow-from" }, | |
120 | { "value", entries }, | |
121 | }); | |
41942bb3 CH |
122 | } |
123 | ||
8171ab83 | 124 | static void fillZone(const DNSName& zonename, HttpResponse* resp) |
02945d9a | 125 | { |
a712cb56 RG |
126 | auto iter = SyncRes::t_sstorage.domainmap->find(zonename); |
127 | if (iter == SyncRes::t_sstorage.domainmap->end()) | |
86f1af1c | 128 | throw ApiException("Could not find domain '"+zonename.toLogString()+"'"); |
02945d9a | 129 | |
02945d9a CH |
130 | const SyncRes::AuthDomain& zone = iter->second; |
131 | ||
15cb63ca CH |
132 | Json::array servers; |
133 | for(const ComboAddress& server : zone.d_servers) { | |
134 | servers.push_back(server.toStringWithPort()); | |
02945d9a | 135 | } |
15cb63ca CH |
136 | |
137 | Json::array records; | |
138 | for(const SyncRes::AuthDomain::records_t::value_type& dr : zone.d_records) { | |
139 | records.push_back(Json::object { | |
140 | { "name", dr.d_name.toString() }, | |
141 | { "type", DNSRecordContent::NumberToType(dr.d_type) }, | |
142 | { "ttl", (double)dr.d_ttl }, | |
143 | { "content", dr.d_content->getZoneRepresentation() } | |
144 | }); | |
02945d9a | 145 | } |
15cb63ca CH |
146 | |
147 | // id is the canonical lookup key, which doesn't actually match the name (in some cases) | |
148 | string zoneId = apiZoneNameToId(iter->first); | |
149 | Json::object doc = { | |
150 | { "id", zoneId }, | |
151 | { "url", "/api/v1/servers/localhost/zones/" + zoneId }, | |
152 | { "name", iter->first.toString() }, | |
153 | { "kind", zone.d_servers.empty() ? "Native" : "Forwarded" }, | |
154 | { "servers", servers }, | |
155 | { "recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward }, | |
156 | { "records", records } | |
157 | }; | |
02945d9a CH |
158 | |
159 | resp->setBody(doc); | |
160 | } | |
161 | ||
15cb63ca | 162 | static void doCreateZone(const Json document) |
02945d9a | 163 | { |
d07bf7ff PL |
164 | if (::arg()["api-config-dir"].empty()) { |
165 | throw ApiException("Config Option \"api-config-dir\" must be set"); | |
02945d9a CH |
166 | } |
167 | ||
c576d0c5 | 168 | DNSName zonename = apiNameToDNSName(stringFromJson(document, "name")); |
1d6b70f9 | 169 | apiCheckNameAllowedCharacters(zonename.toString()); |
02945d9a | 170 | |
15cb63ca | 171 | string singleIPTarget = document["single_target_ip"].string_value(); |
02945d9a CH |
172 | string kind = toUpper(stringFromJson(document, "kind")); |
173 | bool rd = boolFromJson(document, "recursion_desired"); | |
174 | string confbasename = "zone-" + apiZoneNameToId(zonename); | |
175 | ||
176 | if (kind == "NATIVE") { | |
177 | if (rd) | |
178 | throw ApiException("kind=Native and recursion_desired are mutually exclusive"); | |
10e69330 | 179 | if(!singleIPTarget.empty()) { |
180 | try { | |
181 | ComboAddress rem(singleIPTarget); | |
182 | if(rem.sin4.sin_family != AF_INET) | |
183 | throw ApiException(""); | |
184 | singleIPTarget = rem.toString(); | |
185 | } | |
186 | catch(...) { | |
187 | throw ApiException("Single IP target '"+singleIPTarget+"' is invalid"); | |
188 | } | |
189 | } | |
d07bf7ff | 190 | string zonefilename = ::arg()["api-config-dir"] + "/" + confbasename + ".zone"; |
02945d9a CH |
191 | ofstream ofzone(zonefilename.c_str()); |
192 | if (!ofzone) { | |
193 | throw ApiException("Could not open '"+zonefilename+"' for writing: "+stringerror()); | |
194 | } | |
195 | ofzone << "; Generated by pdns-recursor REST API, DO NOT EDIT" << endl; | |
196 | ofzone << zonename << "\tIN\tSOA\tlocal.zone.\thostmaster."<<zonename<<" 1 1 1 1 1" << endl; | |
7f643957 | 197 | if(!singleIPTarget.empty()) { |
10e69330 | 198 | ofzone <<zonename << "\t3600\tIN\tA\t"<<singleIPTarget<<endl; |
199 | ofzone <<"*."<<zonename << "\t3600\tIN\tA\t"<<singleIPTarget<<endl; | |
7f643957 | 200 | } |
02945d9a CH |
201 | ofzone.close(); |
202 | ||
8171ab83 | 203 | apiWriteConfigFile(confbasename, "auth-zones+=" + zonename.toString() + "=" + zonefilename); |
02945d9a | 204 | } else if (kind == "FORWARDED") { |
02945d9a | 205 | string serverlist; |
15cb63ca CH |
206 | for (auto value : document["servers"].array_items()) { |
207 | string server = value.string_value(); | |
208 | if (server == "") { | |
209 | throw ApiException("Forwarded-to server must not be an empty string"); | |
02945d9a | 210 | } |
5f8108e9 RG |
211 | try { |
212 | ComboAddress ca = parseIPAndPort(server, 53); | |
213 | if (!serverlist.empty()) { | |
214 | serverlist += ";"; | |
215 | } | |
216 | serverlist += ca.toStringWithPort(); | |
217 | } catch (const PDNSException &e) { | |
218 | throw ApiException(e.reason); | |
15cb63ca | 219 | } |
02945d9a | 220 | } |
15cb63ca CH |
221 | if (serverlist == "") |
222 | throw ApiException("Need at least one upstream server when forwarding"); | |
02945d9a CH |
223 | |
224 | if (rd) { | |
8171ab83 | 225 | apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename.toString() + "=" + serverlist); |
02945d9a | 226 | } else { |
8171ab83 | 227 | apiWriteConfigFile(confbasename, "forward-zones+=" + zonename.toString() + "=" + serverlist); |
02945d9a CH |
228 | } |
229 | } else { | |
230 | throw ApiException("invalid kind"); | |
231 | } | |
232 | } | |
233 | ||
8171ab83 | 234 | static bool doDeleteZone(const DNSName& zonename) |
02945d9a | 235 | { |
d07bf7ff PL |
236 | if (::arg()["api-config-dir"].empty()) { |
237 | throw ApiException("Config Option \"api-config-dir\" must be set"); | |
02945d9a CH |
238 | } |
239 | ||
240 | string filename; | |
241 | ||
242 | // this one must exist | |
d07bf7ff | 243 | filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf"; |
02945d9a CH |
244 | if (unlink(filename.c_str()) != 0) { |
245 | return false; | |
246 | } | |
247 | ||
248 | // .zone file is optional | |
d07bf7ff | 249 | filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".zone"; |
02945d9a CH |
250 | unlink(filename.c_str()); |
251 | ||
252 | return true; | |
253 | } | |
254 | ||
255 | static void apiServerZones(HttpRequest* req, HttpResponse* resp) | |
256 | { | |
2054afbb | 257 | if (req->method == "POST") { |
d07bf7ff PL |
258 | if (::arg()["api-config-dir"].empty()) { |
259 | throw ApiException("Config Option \"api-config-dir\" must be set"); | |
02945d9a CH |
260 | } |
261 | ||
15cb63ca | 262 | Json document = req->json(); |
02945d9a | 263 | |
c576d0c5 | 264 | DNSName zonename = apiNameToDNSName(stringFromJson(document, "name")); |
02945d9a | 265 | |
a712cb56 RG |
266 | auto iter = SyncRes::t_sstorage.domainmap->find(zonename); |
267 | if (iter != SyncRes::t_sstorage.domainmap->end()) | |
02945d9a CH |
268 | throw ApiException("Zone already exists"); |
269 | ||
270 | doCreateZone(document); | |
271 | reloadAuthAndForwards(); | |
272 | fillZone(zonename, resp); | |
64a36f0d | 273 | resp->status = 201; |
02945d9a CH |
274 | return; |
275 | } | |
276 | ||
277 | if(req->method != "GET") | |
278 | throw HttpMethodNotAllowedException(); | |
279 | ||
15cb63ca | 280 | Json::array doc; |
a712cb56 | 281 | for(const SyncRes::domainmap_t::value_type& val : *SyncRes::t_sstorage.domainmap) { |
02945d9a | 282 | const SyncRes::AuthDomain& zone = val.second; |
15cb63ca CH |
283 | Json::array servers; |
284 | for(const ComboAddress& server : zone.d_servers) { | |
285 | servers.push_back(server.toStringWithPort()); | |
286 | } | |
02945d9a | 287 | // id is the canonical lookup key, which doesn't actually match the name (in some cases) |
8171ab83 | 288 | string zoneId = apiZoneNameToId(val.first); |
15cb63ca CH |
289 | doc.push_back(Json::object { |
290 | { "id", zoneId }, | |
291 | { "url", "/api/v1/servers/localhost/zones/" + zoneId }, | |
292 | { "name", val.first.toString() }, | |
293 | { "kind", zone.d_servers.empty() ? "Native" : "Forwarded" }, | |
294 | { "servers", servers }, | |
295 | { "recursion_desired", zone.d_servers.empty() ? false : zone.d_rdForward } | |
296 | }); | |
02945d9a CH |
297 | } |
298 | resp->setBody(doc); | |
299 | } | |
300 | ||
301 | static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) | |
302 | { | |
8171ab83 | 303 | DNSName zonename = apiZoneIdToName(req->parameters["id"]); |
02945d9a | 304 | |
a712cb56 RG |
305 | SyncRes::domainmap_t::const_iterator iter = SyncRes::t_sstorage.domainmap->find(zonename); |
306 | if (iter == SyncRes::t_sstorage.domainmap->end()) | |
86f1af1c | 307 | throw ApiException("Could not find domain '"+zonename.toLogString()+"'"); |
02945d9a | 308 | |
2054afbb | 309 | if(req->method == "PUT") { |
15cb63ca | 310 | Json document = req->json(); |
02945d9a CH |
311 | |
312 | doDeleteZone(zonename); | |
313 | doCreateZone(document); | |
314 | reloadAuthAndForwards(); | |
f0e76cee CH |
315 | resp->body = ""; |
316 | resp->status = 204; // No Content, but indicate success | |
02945d9a | 317 | } |
2054afbb | 318 | else if(req->method == "DELETE") { |
02945d9a CH |
319 | if (!doDeleteZone(zonename)) { |
320 | throw ApiException("Deleting domain failed"); | |
321 | } | |
322 | ||
323 | reloadAuthAndForwards(); | |
324 | // empty body on success | |
325 | resp->body = ""; | |
37663c3b | 326 | resp->status = 204; // No Content: declare that the zone is gone now |
02945d9a CH |
327 | } else if(req->method == "GET") { |
328 | fillZone(zonename, resp); | |
329 | } else { | |
330 | throw HttpMethodNotAllowedException(); | |
331 | } | |
332 | } | |
333 | ||
37bc3d01 CH |
334 | static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) { |
335 | if(req->method != "GET") | |
336 | throw HttpMethodNotAllowedException(); | |
337 | ||
583ea80d | 338 | string q = req->getvars["q"]; |
37bc3d01 CH |
339 | if (q.empty()) |
340 | throw ApiException("Query q can't be blank"); | |
341 | ||
565a3e28 | 342 | Json::array doc; |
a712cb56 | 343 | for(const SyncRes::domainmap_t::value_type& val : *SyncRes::t_sstorage.domainmap) { |
8171ab83 | 344 | string zoneId = apiZoneNameToId(val.first); |
565a3e28 CH |
345 | string zoneName = val.first.toString(); |
346 | if (pdns_ci_find(zoneName, q) != string::npos) { | |
347 | doc.push_back(Json::object { | |
348 | { "type", "zone" }, | |
349 | { "zone_id", zoneId }, | |
350 | { "name", zoneName } | |
351 | }); | |
37bc3d01 CH |
352 | } |
353 | ||
354 | // if zone name is an exact match, don't bother with returning all records/comments in it | |
8171ab83 | 355 | if (val.first == DNSName(q)) { |
37bc3d01 CH |
356 | continue; |
357 | } | |
358 | ||
359 | const SyncRes::AuthDomain& zone = val.second; | |
360 | ||
565a3e28 | 361 | for(const SyncRes::AuthDomain::records_t::value_type& rr : zone.d_records) { |
e325f20c | 362 | if (pdns_ci_find(rr.d_name.toString(), q) == string::npos && pdns_ci_find(rr.d_content->getZoneRepresentation(), q) == string::npos) |
37bc3d01 CH |
363 | continue; |
364 | ||
565a3e28 CH |
365 | doc.push_back(Json::object { |
366 | { "type", "record" }, | |
367 | { "zone_id", zoneId }, | |
368 | { "zone_name", zoneName }, | |
369 | { "name", rr.d_name.toString() }, | |
370 | { "content", rr.d_content->getZoneRepresentation() } | |
371 | }); | |
37bc3d01 CH |
372 | } |
373 | } | |
374 | resp->setBody(doc); | |
375 | } | |
376 | ||
c0f6a1da | 377 | static void apiServerCacheFlush(HttpRequest* req, HttpResponse* resp) { |
a426cb89 CH |
378 | if(req->method != "PUT") |
379 | throw HttpMethodNotAllowedException(); | |
380 | ||
c0f6a1da | 381 | DNSName canon = apiNameToDNSName(req->getvars["domain"]); |
46d03ec1 | 382 | bool subtree = (req->getvars.count("subtree") > 0 && req->getvars["subtree"].compare("true") == 0); |
c0f6a1da | 383 | |
d19c22a1 CHB |
384 | int count = broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeCache, canon, subtree)); |
385 | count += broadcastAccFunction<uint64_t>(boost::bind(pleaseWipePacketCache, canon, subtree)); | |
386 | count += broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon, subtree)); | |
f682752a CH |
387 | resp->setBody(Json::object { |
388 | { "count", count }, | |
389 | { "result", "Flushed cache." } | |
390 | }); | |
a426cb89 CH |
391 | } |
392 | ||
64c9da8c | 393 | static void apiServerRPZStats(HttpRequest* req, HttpResponse* resp) { |
2278ffda PL |
394 | if(req->method != "GET") |
395 | throw HttpMethodNotAllowedException(); | |
396 | ||
397 | auto luaconf = g_luaconfs.getLocal(); | |
398 | auto numZones = luaconf->dfe.size(); | |
399 | ||
400 | Json::object ret; | |
401 | ||
402 | for (size_t i=0; i < numZones; i++) { | |
403 | auto zone = luaconf->dfe.getZone(i); | |
404 | if (zone == nullptr) | |
405 | continue; | |
406 | auto name = zone->getName(); | |
20c37dec PL |
407 | auto stats = getRPZZoneStats(*name); |
408 | if (stats == nullptr) | |
409 | continue; | |
2278ffda | 410 | Json::object zoneInfo = { |
20c37dec PL |
411 | {"transfers_failed", (double)stats->d_failedTransfers}, |
412 | {"transfers_success", (double)stats->d_successfulTransfers}, | |
413 | {"transfers_full", (double)stats->d_fullTransfers}, | |
414 | {"records", (double)stats->d_numberOfRecords}, | |
415 | {"last_update", (double)stats->d_lastUpdate}, | |
416 | {"serial", (double)stats->d_serial}, | |
2278ffda PL |
417 | }; |
418 | ret[*name] = zoneInfo; | |
419 | } | |
420 | resp->setBody(ret); | |
421 | } | |
422 | ||
642339d8 | 423 | #include "htmlfiles.h" |
d579380d | 424 | |
b184a9dc | 425 | static void serveStuff(HttpRequest* req, HttpResponse* resp) |
d579380d | 426 | { |
427 | resp->headers["Cache-Control"] = "max-age=86400"; | |
d579380d | 428 | |
d887229a | 429 | if(req->url.path == "/") |
430 | req->url.path = "/index.html"; | |
431 | ||
642339d8 | 432 | const string charset = "; charset=utf-8"; |
433 | if(boost::ends_with(req->url.path, ".html")) | |
434 | resp->headers["Content-Type"] = "text/html" + charset; | |
435 | else if(boost::ends_with(req->url.path, ".css")) | |
436 | resp->headers["Content-Type"] = "text/css" + charset; | |
437 | else if(boost::ends_with(req->url.path,".js")) | |
438 | resp->headers["Content-Type"] = "application/javascript" + charset; | |
439 | else if(boost::ends_with(req->url.path, ".png")) | |
440 | resp->headers["Content-Type"] = "image/png"; | |
441 | ||
442 | resp->headers["X-Content-Type-Options"] = "nosniff"; | |
443 | resp->headers["X-Frame-Options"] = "deny"; | |
444 | resp->headers["X-Permitted-Cross-Domain-Policies"] = "none"; | |
445 | ||
446 | resp->headers["X-XSS-Protection"] = "1; mode=block"; | |
447 | // resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'"; | |
448 | ||
449 | resp->body = g_urlmap[req->url.path.c_str()+1]; | |
d579380d | 450 | resp->status = 200; |
451 | } | |
452 | ||
453 | ||
1ce57618 | 454 | RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm) |
a7a902fb | 455 | { |
b0b37121 | 456 | registerAllStats(); |
a7a902fb | 457 | |
d07bf7ff | 458 | d_ws = new AsyncWebServer(fdm, arg()["webserver-address"], arg().asNum("webserver-port")); |
29997a3c | 459 | d_ws->setApiKey(arg()["api-key"]); |
0c3b088c | 460 | d_ws->setPassword(arg()["webserver-password"]); |
8ca656a8 | 461 | d_ws->setLogLevel(arg()["webserver-loglevel"]); |
0010aefa PL |
462 | |
463 | NetmaskGroup acl; | |
464 | acl.toMasks(::arg()["webserver-allow-from"]); | |
465 | d_ws->setACL(acl); | |
466 | ||
825fa717 | 467 | d_ws->bind(); |
867f6abc | 468 | |
3ae143b0 | 469 | // legacy dispatch |
c563cbe5 | 470 | d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2), true); |
c0f6a1da | 471 | d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush); |
46d06a12 PL |
472 | d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-from", &apiServerConfigAllowFrom); |
473 | d_ws->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig); | |
64c9da8c | 474 | d_ws->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", &apiServerRPZStats); |
46d06a12 | 475 | d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData); |
c563cbe5 | 476 | d_ws->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics, true); |
46d06a12 PL |
477 | d_ws->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail); |
478 | d_ws->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones); | |
c563cbe5 | 479 | d_ws->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail, true); |
46d06a12 | 480 | d_ws->registerApiHandler("/api/v1/servers", &apiServer); |
9e6d2033 | 481 | d_ws->registerApiHandler("/api", &apiDiscovery); |
867f6abc | 482 | |
642339d8 | 483 | for(const auto& u : g_urlmap) |
484 | d_ws->registerWebHandler("/"+u.first, serveStuff); | |
d579380d | 485 | d_ws->registerWebHandler("/", serveStuff); |
3ae143b0 | 486 | d_ws->go(); |
867f6abc CH |
487 | } |
488 | ||
1ce57618 | 489 | void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp) |
867f6abc | 490 | { |
e2897a7d | 491 | string command; |
a7a902fb | 492 | |
583ea80d CH |
493 | if(req->getvars.count("command")) { |
494 | command = req->getvars["command"]; | |
495 | req->getvars.erase("command"); | |
3ae143b0 | 496 | } |
a7a902fb | 497 | |
3ddb9247 | 498 | map<string, string> stats; |
a426cb89 | 499 | if(command == "get-query-ring") { |
3ddb9247 | 500 | typedef pair<DNSName,uint16_t> query_t; |
c89f8cd0 | 501 | vector<query_t> queries; |
509196af | 502 | bool filter=!req->getvars["public-filtered"].empty(); |
3ddb9247 | 503 | |
c89f8cd0 | 504 | if(req->getvars["name"]=="servfail-queries") |
505 | queries=broadcastAccFunction<vector<query_t> >(pleaseGetServfailQueryRing); | |
5208e0bf | 506 | else if(req->getvars["name"]=="bogus-queries") |
66f2e6ad | 507 | queries=broadcastAccFunction<vector<query_t> >(pleaseGetBogusQueryRing); |
c89f8cd0 | 508 | else if(req->getvars["name"]=="queries") |
509 | queries=broadcastAccFunction<vector<query_t> >(pleaseGetQueryRing); | |
3ddb9247 | 510 | |
c89f8cd0 | 511 | typedef map<query_t,unsigned int> counts_t; |
512 | counts_t counts; | |
513 | unsigned int total=0; | |
ef7cd021 | 514 | for(const query_t& q : queries) { |
c89f8cd0 | 515 | total++; |
509196af | 516 | if(filter) |
3ddb9247 PD |
517 | counts[make_pair(getRegisteredName(q.first), q.second)]++; |
518 | else | |
519 | counts[make_pair(q.first, q.second)]++; | |
c89f8cd0 | 520 | } |
3ddb9247 | 521 | |
c89f8cd0 | 522 | typedef std::multimap<int, query_t> rcounts_t; |
523 | rcounts_t rcounts; | |
3ddb9247 | 524 | |
c89f8cd0 | 525 | for(counts_t::const_iterator i=counts.begin(); i != counts.end(); ++i) |
526 | rcounts.insert(make_pair(-i->second, i->first)); | |
527 | ||
e0e741df | 528 | Json::array entries; |
509196af | 529 | unsigned int tot=0, totIncluded=0; |
ef7cd021 | 530 | for(const rcounts_t::value_type& q : rcounts) { |
509196af | 531 | totIncluded-=q.first; |
e0e741df | 532 | entries.push_back(Json::array { |
d3ca14b2 | 533 | -q.first, q.second.first.toLogString(), DNSRecordContent::NumberToType(q.second.second) |
e0e741df | 534 | }); |
c89f8cd0 | 535 | if(tot++>=100) |
536 | break; | |
537 | } | |
509196af | 538 | if(queries.size() != totIncluded) { |
e0e741df CH |
539 | entries.push_back(Json::array { |
540 | (int)(queries.size() - totIncluded), "", "" | |
541 | }); | |
509196af | 542 | } |
e0e741df | 543 | resp->setBody(Json::object { { "entries", entries } }); |
509196af | 544 | return; |
545 | } | |
546 | else if(command == "get-remote-ring") { | |
547 | vector<ComboAddress> queries; | |
548 | if(req->getvars["name"]=="remotes") | |
549 | queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetRemotes); | |
550 | else if(req->getvars["name"]=="servfail-remotes") | |
551 | queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetServfailRemotes); | |
66f2e6ad KM |
552 | else if(req->getvars["name"]=="bogus-remotes") |
553 | queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetBogusRemotes); | |
509196af | 554 | else if(req->getvars["name"]=="large-answer-remotes") |
555 | queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetLargeAnswerRemotes); | |
621ccf89 | 556 | else if(req->getvars["name"]=="timeouts") |
557 | queries=broadcastAccFunction<vector<ComboAddress> >(pleaseGetTimeouts); | |
3ddb9247 | 558 | |
509196af | 559 | typedef map<ComboAddress,unsigned int,ComboAddress::addressOnlyLessThan> counts_t; |
560 | counts_t counts; | |
561 | unsigned int total=0; | |
ef7cd021 | 562 | for(const ComboAddress& q : queries) { |
509196af | 563 | total++; |
564 | counts[q]++; | |
565 | } | |
3ddb9247 | 566 | |
509196af | 567 | typedef std::multimap<int, ComboAddress> rcounts_t; |
568 | rcounts_t rcounts; | |
3ddb9247 | 569 | |
509196af | 570 | for(counts_t::const_iterator i=counts.begin(); i != counts.end(); ++i) |
571 | rcounts.insert(make_pair(-i->second, i->first)); | |
572 | ||
e0e741df | 573 | Json::array entries; |
509196af | 574 | unsigned int tot=0, totIncluded=0; |
ef7cd021 | 575 | for(const rcounts_t::value_type& q : rcounts) { |
509196af | 576 | totIncluded-=q.first; |
e0e741df CH |
577 | entries.push_back(Json::array { |
578 | -q.first, q.second.toString() | |
579 | }); | |
509196af | 580 | if(tot++>=100) |
581 | break; | |
582 | } | |
583 | if(queries.size() != totIncluded) { | |
e0e741df | 584 | entries.push_back(Json::array { |
d6dcfe36 | 585 | (int)(queries.size() - totIncluded), "" |
e0e741df | 586 | }); |
509196af | 587 | } |
588 | ||
e0e741df | 589 | resp->setBody(Json::object { { "entries", entries } }); |
c89f8cd0 | 590 | return; |
3ae143b0 | 591 | } else { |
692829aa | 592 | resp->setErrorResult("Command '"+command+"' not found", 404); |
3ae143b0 | 593 | } |
a7a902fb | 594 | } |
825fa717 CH |
595 | |
596 | ||
597 | void AsyncServerNewConnectionMT(void *p) { | |
598 | AsyncServer *server = (AsyncServer*)p; | |
2cfc20bd | 599 | |
825fa717 | 600 | try { |
2cfc20bd | 601 | auto socket = server->accept(); // this is actually a shared_ptr |
8a781bb5 RG |
602 | if (socket) { |
603 | server->d_asyncNewConnectionCallback(socket); | |
604 | } | |
825fa717 CH |
605 | } catch (NetworkError &e) { |
606 | // we're running in a shared process/thread, so can't just terminate/abort. | |
e6a9dde5 | 607 | g_log<<Logger::Warning<<"Network error in web thread: "<<e.what()<<endl; |
825fa717 CH |
608 | return; |
609 | } | |
2cfc20bd | 610 | catch (...) { |
e6a9dde5 | 611 | g_log<<Logger::Warning<<"Unknown error in web thread"<<endl; |
2cfc20bd | 612 | |
613 | return; | |
614 | } | |
615 | ||
825fa717 CH |
616 | } |
617 | ||
618 | void AsyncServer::asyncWaitForConnections(FDMultiplexer* fdm, const newconnectioncb_t& callback) | |
619 | { | |
620 | d_asyncNewConnectionCallback = callback; | |
621 | fdm->addReadFD(d_server_socket.getHandle(), boost::bind(&AsyncServer::newConnection, this)); | |
622 | } | |
623 | ||
624 | void AsyncServer::newConnection() | |
625 | { | |
f165a1f4 | 626 | getMT()->makeThread(&AsyncServerNewConnectionMT, this); |
825fa717 CH |
627 | } |
628 | ||
5d7bd100 | 629 | // This is an entry point from FDM, so it needs to catch everything. |
b030ea34 | 630 | void AsyncWebServer::serveConnection(std::shared_ptr<Socket> client) const { |
f024c7bc | 631 | const string logprefix = d_logprefix + to_string(getUniqueID()) + " "; |
b030ea34 PL |
632 | |
633 | HttpRequest req(logprefix); | |
634 | HttpResponse resp; | |
635 | ComboAddress remote; | |
636 | string reply; | |
637 | ||
825fa717 | 638 | try { |
b030ea34 PL |
639 | YaHTTP::AsyncRequestLoader yarl; |
640 | yarl.initialize(&req); | |
641 | client->setNonBlocking(); | |
642 | ||
643 | string data; | |
644 | try { | |
645 | while(!req.complete) { | |
646 | int bytes = arecvtcp(data, 16384, client.get(), true); | |
647 | if (bytes > 0) { | |
648 | req.complete = yarl.feed(data); | |
649 | } else { | |
650 | // read error OR EOF | |
651 | break; | |
652 | } | |
825fa717 | 653 | } |
b030ea34 PL |
654 | yarl.finalize(); |
655 | } catch (YaHTTP::ParseError &e) { | |
656 | // request stays incomplete | |
9fc86908 | 657 | g_log<<Logger::Warning<<logprefix<<"Unable to parse request: "<<e.what()<<endl; |
b030ea34 PL |
658 | } |
659 | ||
660 | if (d_loglevel >= WebServer::LogLevel::None) { | |
661 | client->getRemote(remote); | |
825fa717 | 662 | } |
b030ea34 | 663 | |
612ad9ec | 664 | logRequest(req, remote); |
b030ea34 PL |
665 | |
666 | WebServer::handleRequest(req, resp); | |
667 | ostringstream ss; | |
668 | resp.write(ss); | |
669 | reply = ss.str(); | |
670 | ||
612ad9ec | 671 | logResponse(resp, remote, logprefix); |
b030ea34 PL |
672 | |
673 | // now send the reply | |
674 | if (asendtcp(reply, client.get()) == -1 || reply.empty()) { | |
675 | g_log<<Logger::Error<<logprefix<<"Failed sending reply to HTTP client"<<endl; | |
676 | } | |
677 | } | |
678 | catch(PDNSException &e) { | |
679 | g_log<<Logger::Error<<logprefix<<"Exception: "<<e.reason<<endl; | |
680 | } | |
681 | catch(std::exception &e) { | |
682 | if(strstr(e.what(), "timeout")==0) | |
683 | g_log<<Logger::Error<<logprefix<<"STL Exception: "<<e.what()<<endl; | |
684 | } | |
685 | catch(...) { | |
686 | g_log<<Logger::Error<<logprefix<<"Unknown exception"<<endl; | |
825fa717 CH |
687 | } |
688 | ||
b030ea34 PL |
689 | if (d_loglevel >= WebServer::LogLevel::Normal) { |
690 | g_log<<Logger::Notice<<logprefix<<remote<<" \""<<req.method<<" "<<req.url.path<<" HTTP/"<<req.versionStr(req.version)<<"\" "<<resp.status<<" "<<reply.size()<<endl; | |
825fa717 CH |
691 | } |
692 | } | |
693 | ||
694 | void AsyncWebServer::go() { | |
695 | if (!d_server) | |
696 | return; | |
690984d4 RG |
697 | auto server = std::dynamic_pointer_cast<AsyncServer>(d_server); |
698 | if (!server) | |
699 | return; | |
700 | server->asyncWaitForConnections(d_fdm, boost::bind(&AsyncWebServer::serveConnection, this, _1)); | |
825fa717 | 701 | } |