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