2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
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.
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.
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.
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.
25 #include "ws-recursor.hh"
29 #include "namespaces.hh"
32 #include "rec_channel.hh"
33 #include "arguments.hh"
36 #include "dnsparser.hh"
38 #include "webserver.hh"
41 #include "ext/incbin/incbin.h"
42 #include "rec-lua-conf.hh"
43 #include "rpzloader.hh"
45 extern thread_local FDMultiplexer
* t_fdm
;
49 void productServerStatisticsFetch(map
<string
,string
>& out
)
51 map
<string
,string
> stats
= getAllStatsMap();
55 static void apiWriteConfigFile(const string
& filebasename
, const string
& content
)
57 if (::arg()["api-config-dir"].empty()) {
58 throw ApiException("Config Option \"api-config-dir\" must be set");
61 string filename
= ::arg()["api-config-dir"] + "/" + filebasename
+ ".conf";
62 ofstream
ofconf(filename
.c_str());
64 throw ApiException("Could not open config fragment file '"+filename
+"' for writing: "+stringerror());
66 ofconf
<< "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl
;
67 ofconf
<< content
<< endl
;
71 static void apiServerConfigAllowFrom(HttpRequest
* req
, HttpResponse
* resp
)
73 if (req
->method
== "PUT" && !::arg().mustDo("api-readonly")) {
74 Json document
= req
->json();
76 auto jlist
= document
["value"];
77 if (!jlist
.is_array()) {
78 throw ApiException("'value' must be an array");
82 for (auto value
: jlist
.array_items()) {
84 nmg
.addMask(value
.string_value());
85 } catch (const NetmaskException
&e
) {
86 throw ApiException(e
.reason
);
92 // Clear allow-from-file if set, so our changes take effect
93 ss
<< "allow-from-file=" << endl
;
95 // Clear allow-from, and provide a "parent" value
96 ss
<< "allow-from=" << endl
;
97 ss
<< "allow-from+=" << nmg
.toString() << endl
;
99 apiWriteConfigFile("allow-from", ss
.str());
103 // fall through to GET
104 } else if (req
->method
!= "GET") {
105 throw HttpMethodNotAllowedException();
108 // Return currently configured ACLs
109 vector
<string
> entries
;
110 t_allowFrom
->toStringVector(&entries
);
112 resp
->setBody(Json::object
{
113 { "name", "allow-from" },
114 { "value", entries
},
118 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
)
120 auto iter
= SyncRes::t_sstorage
.domainmap
->find(zonename
);
121 if (iter
== SyncRes::t_sstorage
.domainmap
->end())
122 throw ApiException("Could not find domain '"+zonename
.toLogString()+"'");
124 const SyncRes::AuthDomain
& zone
= iter
->second
;
127 for(const ComboAddress
& server
: zone
.d_servers
) {
128 servers
.push_back(server
.toStringWithPort());
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() }
141 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
142 string zoneId
= apiZoneNameToId(iter
->first
);
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
}
156 static void doCreateZone(const Json document
)
158 if (::arg()["api-config-dir"].empty()) {
159 throw ApiException("Config Option \"api-config-dir\" must be set");
162 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
163 apiCheckNameAllowedCharacters(zonename
.toString());
165 string singleIPTarget
= document
["single_target_ip"].string_value();
166 string kind
= toUpper(stringFromJson(document
, "kind"));
167 bool rd
= boolFromJson(document
, "recursion_desired");
168 string confbasename
= "zone-" + apiZoneNameToId(zonename
);
170 if (kind
== "NATIVE") {
172 throw ApiException("kind=Native and recursion_desired are mutually exclusive");
173 if(!singleIPTarget
.empty()) {
175 ComboAddress
rem(singleIPTarget
);
176 if(rem
.sin4
.sin_family
!= AF_INET
)
177 throw ApiException("");
178 singleIPTarget
= rem
.toString();
181 throw ApiException("Single IP target '"+singleIPTarget
+"' is invalid");
184 string zonefilename
= ::arg()["api-config-dir"] + "/" + confbasename
+ ".zone";
185 ofstream
ofzone(zonefilename
.c_str());
187 throw ApiException("Could not open '"+zonefilename
+"' for writing: "+stringerror());
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
;
191 if(!singleIPTarget
.empty()) {
192 ofzone
<<zonename
<< "\t3600\tIN\tA\t"<<singleIPTarget
<<endl
;
193 ofzone
<<"*."<<zonename
<< "\t3600\tIN\tA\t"<<singleIPTarget
<<endl
;
197 apiWriteConfigFile(confbasename
, "auth-zones+=" + zonename
.toString() + "=" + zonefilename
);
198 } else if (kind
== "FORWARDED") {
200 for (auto value
: document
["servers"].array_items()) {
201 string server
= value
.string_value();
203 throw ApiException("Forwarded-to server must not be an empty string");
206 ComboAddress ca
= parseIPAndPort(server
, 53);
207 if (!serverlist
.empty()) {
210 serverlist
+= ca
.toStringWithPort();
211 } catch (const PDNSException
&e
) {
212 throw ApiException(e
.reason
);
215 if (serverlist
== "")
216 throw ApiException("Need at least one upstream server when forwarding");
219 apiWriteConfigFile(confbasename
, "forward-zones-recurse+=" + zonename
.toString() + "=" + serverlist
);
221 apiWriteConfigFile(confbasename
, "forward-zones+=" + zonename
.toString() + "=" + serverlist
);
224 throw ApiException("invalid kind");
228 static bool doDeleteZone(const DNSName
& zonename
)
230 if (::arg()["api-config-dir"].empty()) {
231 throw ApiException("Config Option \"api-config-dir\" must be set");
236 // this one must exist
237 filename
= ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename
) + ".conf";
238 if (unlink(filename
.c_str()) != 0) {
242 // .zone file is optional
243 filename
= ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename
) + ".zone";
244 unlink(filename
.c_str());
249 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
)
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");
256 Json document
= req
->json();
258 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
260 auto iter
= SyncRes::t_sstorage
.domainmap
->find(zonename
);
261 if (iter
!= SyncRes::t_sstorage
.domainmap
->end())
262 throw ApiException("Zone already exists");
264 doCreateZone(document
);
265 reloadAuthAndForwards();
266 fillZone(zonename
, resp
);
271 if(req
->method
!= "GET")
272 throw HttpMethodNotAllowedException();
275 for(const SyncRes::domainmap_t::value_type
& val
: *SyncRes::t_sstorage
.domainmap
) {
276 const SyncRes::AuthDomain
& zone
= val
.second
;
278 for(const ComboAddress
& server
: zone
.d_servers
) {
279 servers
.push_back(server
.toStringWithPort());
281 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
282 string zoneId
= apiZoneNameToId(val
.first
);
283 doc
.push_back(Json::object
{
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
}
295 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
)
297 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
299 SyncRes::domainmap_t::const_iterator iter
= SyncRes::t_sstorage
.domainmap
->find(zonename
);
300 if (iter
== SyncRes::t_sstorage
.domainmap
->end())
301 throw ApiException("Could not find domain '"+zonename
.toLogString()+"'");
303 if(req
->method
== "PUT" && !::arg().mustDo("api-readonly")) {
304 Json document
= req
->json();
306 doDeleteZone(zonename
);
307 doCreateZone(document
);
308 reloadAuthAndForwards();
310 resp
->status
= 204; // No Content, but indicate success
312 else if(req
->method
== "DELETE" && !::arg().mustDo("api-readonly")) {
313 if (!doDeleteZone(zonename
)) {
314 throw ApiException("Deleting domain failed");
317 reloadAuthAndForwards();
318 // empty body on success
320 resp
->status
= 204; // No Content: declare that the zone is gone now
321 } else if(req
->method
== "GET") {
322 fillZone(zonename
, resp
);
324 throw HttpMethodNotAllowedException();
328 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
329 if(req
->method
!= "GET")
330 throw HttpMethodNotAllowedException();
332 string q
= req
->getvars
["q"];
334 throw ApiException("Query q can't be blank");
337 for(const SyncRes::domainmap_t::value_type
& val
: *SyncRes::t_sstorage
.domainmap
) {
338 string zoneId
= apiZoneNameToId(val
.first
);
339 string zoneName
= val
.first
.toString();
340 if (pdns_ci_find(zoneName
, q
) != string::npos
) {
341 doc
.push_back(Json::object
{
343 { "zone_id", zoneId
},
348 // if zone name is an exact match, don't bother with returning all records/comments in it
349 if (val
.first
== DNSName(q
)) {
353 const SyncRes::AuthDomain
& zone
= val
.second
;
355 for(const SyncRes::AuthDomain::records_t::value_type
& rr
: zone
.d_records
) {
356 if (pdns_ci_find(rr
.d_name
.toString(), q
) == string::npos
&& pdns_ci_find(rr
.d_content
->getZoneRepresentation(), q
) == string::npos
)
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() }
371 static void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
372 if(req
->method
!= "PUT")
373 throw HttpMethodNotAllowedException();
375 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
377 int count
= broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipeCache
, canon
, false));
378 count
+= broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipePacketCache
, canon
, false));
379 count
+= broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipeAndCountNegCache
, canon
, false));
380 resp
->setBody(Json::object
{
382 { "result", "Flushed cache." }
386 static void apiServerRPZStats(HttpRequest
* req
, HttpResponse
* resp
) {
387 if(req
->method
!= "GET")
388 throw HttpMethodNotAllowedException();
390 auto luaconf
= g_luaconfs
.getLocal();
391 auto numZones
= luaconf
->dfe
.size();
395 for (size_t i
=0; i
< numZones
; i
++) {
396 auto zone
= luaconf
->dfe
.getZone(i
);
399 auto name
= zone
->getName();
400 auto stats
= getRPZZoneStats(*name
);
401 if (stats
== nullptr)
403 Json::object zoneInfo
= {
404 {"transfers_failed", (double)stats
->d_failedTransfers
},
405 {"transfers_success", (double)stats
->d_successfulTransfers
},
406 {"transfers_full", (double)stats
->d_fullTransfers
},
407 {"records", (double)stats
->d_numberOfRecords
},
408 {"last_update", (double)stats
->d_lastUpdate
},
409 {"serial", (double)stats
->d_serial
},
411 ret
[*name
] = zoneInfo
;
416 #include "htmlfiles.h"
418 static void serveStuff(HttpRequest
* req
, HttpResponse
* resp
)
420 resp
->headers
["Cache-Control"] = "max-age=86400";
422 if(req
->url
.path
== "/")
423 req
->url
.path
= "/index.html";
425 const string charset
= "; charset=utf-8";
426 if(boost::ends_with(req
->url
.path
, ".html"))
427 resp
->headers
["Content-Type"] = "text/html" + charset
;
428 else if(boost::ends_with(req
->url
.path
, ".css"))
429 resp
->headers
["Content-Type"] = "text/css" + charset
;
430 else if(boost::ends_with(req
->url
.path
,".js"))
431 resp
->headers
["Content-Type"] = "application/javascript" + charset
;
432 else if(boost::ends_with(req
->url
.path
, ".png"))
433 resp
->headers
["Content-Type"] = "image/png";
435 resp
->headers
["X-Content-Type-Options"] = "nosniff";
436 resp
->headers
["X-Frame-Options"] = "deny";
437 resp
->headers
["X-Permitted-Cross-Domain-Policies"] = "none";
439 resp
->headers
["X-XSS-Protection"] = "1; mode=block";
440 // resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
442 resp
->body
= g_urlmap
[req
->url
.path
.c_str()+1];
447 RecursorWebServer::RecursorWebServer(FDMultiplexer
* fdm
)
451 d_ws
= new AsyncWebServer(fdm
, arg()["webserver-address"], arg().asNum("webserver-port"));
455 d_ws
->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat
, this, _1
, _2
));
456 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
457 d_ws
->registerApiHandler("/api/v1/servers/localhost/config/allow-from", &apiServerConfigAllowFrom
);
458 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
459 d_ws
->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", &apiServerRPZStats
);
460 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-log", &apiServerSearchLog
);
461 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
462 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
);
463 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
464 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
465 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
);
466 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
467 d_ws
->registerApiHandler("/api", &apiDiscovery
);
469 for(const auto& u
: g_urlmap
)
470 d_ws
->registerWebHandler("/"+u
.first
, serveStuff
);
471 d_ws
->registerWebHandler("/", serveStuff
);
475 void RecursorWebServer::jsonstat(HttpRequest
* req
, HttpResponse
*resp
)
479 if(req
->getvars
.count("command")) {
480 command
= req
->getvars
["command"];
481 req
->getvars
.erase("command");
484 map
<string
, string
> stats
;
485 if(command
== "get-query-ring") {
486 typedef pair
<DNSName
,uint16_t> query_t
;
487 vector
<query_t
> queries
;
488 bool filter
=!req
->getvars
["public-filtered"].empty();
490 if(req
->getvars
["name"]=="servfail-queries")
491 queries
=broadcastAccFunction
<vector
<query_t
> >(pleaseGetServfailQueryRing
);
492 else if(req
->getvars
["name"]=="queries")
493 queries
=broadcastAccFunction
<vector
<query_t
> >(pleaseGetQueryRing
);
495 typedef map
<query_t
,unsigned int> counts_t
;
497 unsigned int total
=0;
498 for(const query_t
& q
: queries
) {
501 counts
[make_pair(getRegisteredName(q
.first
), q
.second
)]++;
503 counts
[make_pair(q
.first
, q
.second
)]++;
506 typedef std::multimap
<int, query_t
> rcounts_t
;
509 for(counts_t::const_iterator i
=counts
.begin(); i
!= counts
.end(); ++i
)
510 rcounts
.insert(make_pair(-i
->second
, i
->first
));
513 unsigned int tot
=0, totIncluded
=0;
514 for(const rcounts_t::value_type
& q
: rcounts
) {
515 totIncluded
-=q
.first
;
516 entries
.push_back(Json::array
{
517 -q
.first
, q
.second
.first
.toString(), DNSRecordContent::NumberToType(q
.second
.second
)
522 if(queries
.size() != totIncluded
) {
523 entries
.push_back(Json::array
{
524 (int)(queries
.size() - totIncluded
), "", ""
527 resp
->setBody(Json::object
{ { "entries", entries
} });
530 else if(command
== "get-remote-ring") {
531 vector
<ComboAddress
> queries
;
532 if(req
->getvars
["name"]=="remotes")
533 queries
=broadcastAccFunction
<vector
<ComboAddress
> >(pleaseGetRemotes
);
534 else if(req
->getvars
["name"]=="servfail-remotes")
535 queries
=broadcastAccFunction
<vector
<ComboAddress
> >(pleaseGetServfailRemotes
);
536 else if(req
->getvars
["name"]=="large-answer-remotes")
537 queries
=broadcastAccFunction
<vector
<ComboAddress
> >(pleaseGetLargeAnswerRemotes
);
539 typedef map
<ComboAddress
,unsigned int,ComboAddress::addressOnlyLessThan
> counts_t
;
541 unsigned int total
=0;
542 for(const ComboAddress
& q
: queries
) {
547 typedef std::multimap
<int, ComboAddress
> rcounts_t
;
550 for(counts_t::const_iterator i
=counts
.begin(); i
!= counts
.end(); ++i
)
551 rcounts
.insert(make_pair(-i
->second
, i
->first
));
554 unsigned int tot
=0, totIncluded
=0;
555 for(const rcounts_t::value_type
& q
: rcounts
) {
556 totIncluded
-=q
.first
;
557 entries
.push_back(Json::array
{
558 -q
.first
, q
.second
.toString()
563 if(queries
.size() != totIncluded
) {
564 entries
.push_back(Json::array
{
565 (int)(queries
.size() - totIncluded
), ""
569 resp
->setBody(Json::object
{ { "entries", entries
} });
572 resp
->setErrorResult("Command '"+command
+"' not found", 404);
577 void AsyncServerNewConnectionMT(void *p
) {
578 AsyncServer
*server
= (AsyncServer
*)p
;
581 auto socket
= server
->accept(); // this is actually a shared_ptr
583 server
->d_asyncNewConnectionCallback(socket
);
585 } catch (NetworkError
&e
) {
586 // we're running in a shared process/thread, so can't just terminate/abort.
587 L
<<Logger::Warning
<<"Network error in web thread: "<<e
.what()<<endl
;
591 L
<<Logger::Warning
<<"Unknown error in web thread"<<endl
;
598 void AsyncServer::asyncWaitForConnections(FDMultiplexer
* fdm
, const newconnectioncb_t
& callback
)
600 d_asyncNewConnectionCallback
= callback
;
601 fdm
->addReadFD(d_server_socket
.getHandle(), boost::bind(&AsyncServer::newConnection
, this));
604 void AsyncServer::newConnection()
606 getMT()->makeThread(&AsyncServerNewConnectionMT
, this);
609 // This is an entry point from FDM, so it needs to catch everything.
610 void AsyncWebServer::serveConnection(std::shared_ptr
<Socket
> client
) const
613 YaHTTP::AsyncRequestLoader yarl
;
614 yarl
.initialize(&req
);
615 client
->setNonBlocking();
619 while(!req
.complete
) {
620 int bytes
= arecvtcp(data
, 16384, client
.get(), true);
622 req
.complete
= yarl
.feed(data
);
629 } catch (YaHTTP::ParseError
&e
) {
630 // request stays incomplete
634 handleRequest(req
, resp
);
639 // now send the reply
640 if (asendtcp(data
, client
.get()) == -1 || data
.empty()) {
641 L
<<Logger::Error
<<"Failed sending reply to HTTP client"<<endl
;
644 catch(PDNSException
&e
) {
645 L
<<Logger::Error
<<"HTTP Exception: "<<e
.reason
<<endl
;
647 catch(std::exception
&e
) {
648 if(strstr(e
.what(), "timeout")==0)
649 L
<<Logger::Error
<<"HTTP STL Exception: "<<e
.what()<<endl
;
652 L
<<Logger::Error
<<"HTTP: Unknown exception"<<endl
;
655 void AsyncWebServer::go() {
658 auto server
= std::dynamic_pointer_cast
<AsyncServer
>(d_server
);
661 server
->asyncWaitForConnections(d_fdm
, boost::bind(&AsyncWebServer::serveConnection
, this, _1
));