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"
44 #include "uuid-utils.hh"
46 extern thread_local FDMultiplexer
* t_fdm
;
50 void productServerStatisticsFetch(map
<string
,string
>& out
)
52 map
<string
,string
> stats
= getAllStatsMap(StatComponent::API
);
56 boost::optional
<uint64_t> productServerStatisticsFetch(const std::string
& name
)
58 return getStatByName(name
);
61 static void apiWriteConfigFile(const string
& filebasename
, const string
& content
)
63 if (::arg()["api-config-dir"].empty()) {
64 throw ApiException("Config Option \"api-config-dir\" must be set");
67 string filename
= ::arg()["api-config-dir"] + "/" + filebasename
+ ".conf";
68 ofstream
ofconf(filename
.c_str());
70 throw ApiException("Could not open config fragment file '"+filename
+"' for writing: "+stringerror());
72 ofconf
<< "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl
;
73 ofconf
<< content
<< endl
;
77 static void apiServerConfigAllowFrom(HttpRequest
* req
, HttpResponse
* resp
)
79 if (req
->method
== "PUT") {
80 Json document
= req
->json();
82 auto jlist
= document
["value"];
83 if (!jlist
.is_array()) {
84 throw ApiException("'value' must be an array");
88 for (auto value
: jlist
.array_items()) {
90 nmg
.addMask(value
.string_value());
91 } catch (const NetmaskException
&e
) {
92 throw ApiException(e
.reason
);
98 // Clear allow-from-file if set, so our changes take effect
99 ss
<< "allow-from-file=" << endl
;
101 // Clear allow-from, and provide a "parent" value
102 ss
<< "allow-from=" << endl
;
103 ss
<< "allow-from+=" << nmg
.toString() << endl
;
105 apiWriteConfigFile("allow-from", ss
.str());
109 // fall through to GET
110 } else if (req
->method
!= "GET") {
111 throw HttpMethodNotAllowedException();
114 // Return currently configured ACLs
115 vector
<string
> entries
;
116 t_allowFrom
->toStringVector(&entries
);
118 resp
->setBody(Json::object
{
119 { "name", "allow-from" },
120 { "value", entries
},
124 static void fillZone(const DNSName
& zonename
, HttpResponse
* resp
)
126 auto iter
= SyncRes::t_sstorage
.domainmap
->find(zonename
);
127 if (iter
== SyncRes::t_sstorage
.domainmap
->end())
128 throw ApiException("Could not find domain '"+zonename
.toLogString()+"'");
130 const SyncRes::AuthDomain
& zone
= iter
->second
;
133 for(const ComboAddress
& server
: zone
.d_servers
) {
134 servers
.push_back(server
.toStringWithPort());
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() }
147 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
148 string zoneId
= apiZoneNameToId(iter
->first
);
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
}
162 static void doCreateZone(const Json document
)
164 if (::arg()["api-config-dir"].empty()) {
165 throw ApiException("Config Option \"api-config-dir\" must be set");
168 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
169 apiCheckNameAllowedCharacters(zonename
.toString());
171 string singleIPTarget
= document
["single_target_ip"].string_value();
172 string kind
= toUpper(stringFromJson(document
, "kind"));
173 bool rd
= boolFromJson(document
, "recursion_desired");
174 string confbasename
= "zone-" + apiZoneNameToId(zonename
);
176 if (kind
== "NATIVE") {
178 throw ApiException("kind=Native and recursion_desired are mutually exclusive");
179 if(!singleIPTarget
.empty()) {
181 ComboAddress
rem(singleIPTarget
);
182 if(rem
.sin4
.sin_family
!= AF_INET
)
183 throw ApiException("");
184 singleIPTarget
= rem
.toString();
187 throw ApiException("Single IP target '"+singleIPTarget
+"' is invalid");
190 string zonefilename
= ::arg()["api-config-dir"] + "/" + confbasename
+ ".zone";
191 ofstream
ofzone(zonefilename
.c_str());
193 throw ApiException("Could not open '"+zonefilename
+"' for writing: "+stringerror());
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
;
197 if(!singleIPTarget
.empty()) {
198 ofzone
<<zonename
<< "\t3600\tIN\tA\t"<<singleIPTarget
<<endl
;
199 ofzone
<<"*."<<zonename
<< "\t3600\tIN\tA\t"<<singleIPTarget
<<endl
;
203 apiWriteConfigFile(confbasename
, "auth-zones+=" + zonename
.toString() + "=" + zonefilename
);
204 } else if (kind
== "FORWARDED") {
206 for (auto value
: document
["servers"].array_items()) {
207 string server
= value
.string_value();
209 throw ApiException("Forwarded-to server must not be an empty string");
212 ComboAddress ca
= parseIPAndPort(server
, 53);
213 if (!serverlist
.empty()) {
216 serverlist
+= ca
.toStringWithPort();
217 } catch (const PDNSException
&e
) {
218 throw ApiException(e
.reason
);
221 if (serverlist
== "")
222 throw ApiException("Need at least one upstream server when forwarding");
225 apiWriteConfigFile(confbasename
, "forward-zones-recurse+=" + zonename
.toString() + "=" + serverlist
);
227 apiWriteConfigFile(confbasename
, "forward-zones+=" + zonename
.toString() + "=" + serverlist
);
230 throw ApiException("invalid kind");
234 static bool doDeleteZone(const DNSName
& zonename
)
236 if (::arg()["api-config-dir"].empty()) {
237 throw ApiException("Config Option \"api-config-dir\" must be set");
242 // this one must exist
243 filename
= ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename
) + ".conf";
244 if (unlink(filename
.c_str()) != 0) {
248 // .zone file is optional
249 filename
= ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename
) + ".zone";
250 unlink(filename
.c_str());
255 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
)
257 if (req
->method
== "POST") {
258 if (::arg()["api-config-dir"].empty()) {
259 throw ApiException("Config Option \"api-config-dir\" must be set");
262 Json document
= req
->json();
264 DNSName zonename
= apiNameToDNSName(stringFromJson(document
, "name"));
266 auto iter
= SyncRes::t_sstorage
.domainmap
->find(zonename
);
267 if (iter
!= SyncRes::t_sstorage
.domainmap
->end())
268 throw ApiException("Zone already exists");
270 doCreateZone(document
);
271 reloadAuthAndForwards();
272 fillZone(zonename
, resp
);
277 if(req
->method
!= "GET")
278 throw HttpMethodNotAllowedException();
281 for(const SyncRes::domainmap_t::value_type
& val
: *SyncRes::t_sstorage
.domainmap
) {
282 const SyncRes::AuthDomain
& zone
= val
.second
;
284 for(const ComboAddress
& server
: zone
.d_servers
) {
285 servers
.push_back(server
.toStringWithPort());
287 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
288 string zoneId
= apiZoneNameToId(val
.first
);
289 doc
.push_back(Json::object
{
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
}
301 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
)
303 DNSName zonename
= apiZoneIdToName(req
->parameters
["id"]);
305 SyncRes::domainmap_t::const_iterator iter
= SyncRes::t_sstorage
.domainmap
->find(zonename
);
306 if (iter
== SyncRes::t_sstorage
.domainmap
->end())
307 throw ApiException("Could not find domain '"+zonename
.toLogString()+"'");
309 if(req
->method
== "PUT") {
310 Json document
= req
->json();
312 doDeleteZone(zonename
);
313 doCreateZone(document
);
314 reloadAuthAndForwards();
316 resp
->status
= 204; // No Content, but indicate success
318 else if(req
->method
== "DELETE") {
319 if (!doDeleteZone(zonename
)) {
320 throw ApiException("Deleting domain failed");
323 reloadAuthAndForwards();
324 // empty body on success
326 resp
->status
= 204; // No Content: declare that the zone is gone now
327 } else if(req
->method
== "GET") {
328 fillZone(zonename
, resp
);
330 throw HttpMethodNotAllowedException();
334 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
335 if(req
->method
!= "GET")
336 throw HttpMethodNotAllowedException();
338 string q
= req
->getvars
["q"];
340 throw ApiException("Query q can't be blank");
343 for(const SyncRes::domainmap_t::value_type
& val
: *SyncRes::t_sstorage
.domainmap
) {
344 string zoneId
= apiZoneNameToId(val
.first
);
345 string zoneName
= val
.first
.toString();
346 if (pdns_ci_find(zoneName
, q
) != string::npos
) {
347 doc
.push_back(Json::object
{
349 { "zone_id", zoneId
},
354 // if zone name is an exact match, don't bother with returning all records/comments in it
355 if (val
.first
== DNSName(q
)) {
359 const SyncRes::AuthDomain
& zone
= val
.second
;
361 for(const SyncRes::AuthDomain::records_t::value_type
& rr
: zone
.d_records
) {
362 if (pdns_ci_find(rr
.d_name
.toString(), q
) == string::npos
&& pdns_ci_find(rr
.d_content
->getZoneRepresentation(), q
) == string::npos
)
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() }
377 static void apiServerCacheFlush(HttpRequest
* req
, HttpResponse
* resp
) {
378 if(req
->method
!= "PUT")
379 throw HttpMethodNotAllowedException();
381 DNSName canon
= apiNameToDNSName(req
->getvars
["domain"]);
382 bool subtree
= (req
->getvars
.count("subtree") > 0 && req
->getvars
["subtree"].compare("true") == 0);
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
));
387 resp
->setBody(Json::object
{
389 { "result", "Flushed cache." }
393 static void apiServerRPZStats(HttpRequest
* req
, HttpResponse
* resp
) {
394 if(req
->method
!= "GET")
395 throw HttpMethodNotAllowedException();
397 auto luaconf
= g_luaconfs
.getLocal();
398 auto numZones
= luaconf
->dfe
.size();
402 for (size_t i
=0; i
< numZones
; i
++) {
403 auto zone
= luaconf
->dfe
.getZone(i
);
406 auto name
= zone
->getName();
407 auto stats
= getRPZZoneStats(*name
);
408 if (stats
== nullptr)
410 Json::object zoneInfo
= {
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
},
418 ret
[*name
] = zoneInfo
;
423 #include "htmlfiles.h"
425 static void serveStuff(HttpRequest
* req
, HttpResponse
* resp
)
427 resp
->headers
["Cache-Control"] = "max-age=86400";
429 if(req
->url
.path
== "/")
430 req
->url
.path
= "/index.html";
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";
442 resp
->headers
["X-Content-Type-Options"] = "nosniff";
443 resp
->headers
["X-Frame-Options"] = "deny";
444 resp
->headers
["X-Permitted-Cross-Domain-Policies"] = "none";
446 resp
->headers
["X-XSS-Protection"] = "1; mode=block";
447 // resp->headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self' 'unsafe-inline'";
449 resp
->body
= g_urlmap
[req
->url
.path
.c_str()+1];
454 RecursorWebServer::RecursorWebServer(FDMultiplexer
* fdm
)
458 d_ws
= new AsyncWebServer(fdm
, arg()["webserver-address"], arg().asNum("webserver-port"));
459 d_ws
->setApiKey(arg()["api-key"]);
460 d_ws
->setPassword(arg()["webserver-password"]);
461 d_ws
->setLogLevel(arg()["webserver-loglevel"]);
464 acl
.toMasks(::arg()["webserver-allow-from"]);
470 d_ws
->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat
, this, _1
, _2
), true);
471 d_ws
->registerApiHandler("/api/v1/servers/localhost/cache/flush", &apiServerCacheFlush
);
472 d_ws
->registerApiHandler("/api/v1/servers/localhost/config/allow-from", &apiServerConfigAllowFrom
);
473 d_ws
->registerApiHandler("/api/v1/servers/localhost/config", &apiServerConfig
);
474 d_ws
->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", &apiServerRPZStats
);
475 d_ws
->registerApiHandler("/api/v1/servers/localhost/search-data", &apiServerSearchData
);
476 d_ws
->registerApiHandler("/api/v1/servers/localhost/statistics", &apiServerStatistics
, true);
477 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones/<id>", &apiServerZoneDetail
);
478 d_ws
->registerApiHandler("/api/v1/servers/localhost/zones", &apiServerZones
);
479 d_ws
->registerApiHandler("/api/v1/servers/localhost", &apiServerDetail
, true);
480 d_ws
->registerApiHandler("/api/v1/servers", &apiServer
);
481 d_ws
->registerApiHandler("/api", &apiDiscovery
);
483 for(const auto& u
: g_urlmap
)
484 d_ws
->registerWebHandler("/"+u
.first
, serveStuff
);
485 d_ws
->registerWebHandler("/", serveStuff
);
489 void RecursorWebServer::jsonstat(HttpRequest
* req
, HttpResponse
*resp
)
493 if(req
->getvars
.count("command")) {
494 command
= req
->getvars
["command"];
495 req
->getvars
.erase("command");
498 map
<string
, string
> stats
;
499 if(command
== "get-query-ring") {
500 typedef pair
<DNSName
,uint16_t> query_t
;
501 vector
<query_t
> queries
;
502 bool filter
=!req
->getvars
["public-filtered"].empty();
504 if(req
->getvars
["name"]=="servfail-queries")
505 queries
=broadcastAccFunction
<vector
<query_t
> >(pleaseGetServfailQueryRing
);
506 else if(req
->getvars
["name"]=="bogus-queries")
507 queries
=broadcastAccFunction
<vector
<query_t
> >(pleaseGetBogusQueryRing
);
508 else if(req
->getvars
["name"]=="queries")
509 queries
=broadcastAccFunction
<vector
<query_t
> >(pleaseGetQueryRing
);
511 typedef map
<query_t
,unsigned int> counts_t
;
513 unsigned int total
=0;
514 for(const query_t
& q
: queries
) {
517 counts
[make_pair(getRegisteredName(q
.first
), q
.second
)]++;
519 counts
[make_pair(q
.first
, q
.second
)]++;
522 typedef std::multimap
<int, query_t
> rcounts_t
;
525 for(counts_t::const_iterator i
=counts
.begin(); i
!= counts
.end(); ++i
)
526 rcounts
.insert(make_pair(-i
->second
, i
->first
));
529 unsigned int tot
=0, totIncluded
=0;
530 for(const rcounts_t::value_type
& q
: rcounts
) {
531 totIncluded
-=q
.first
;
532 entries
.push_back(Json::array
{
533 -q
.first
, q
.second
.first
.toLogString(), DNSRecordContent::NumberToType(q
.second
.second
)
538 if(queries
.size() != totIncluded
) {
539 entries
.push_back(Json::array
{
540 (int)(queries
.size() - totIncluded
), "", ""
543 resp
->setBody(Json::object
{ { "entries", entries
} });
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
);
552 else if(req
->getvars
["name"]=="bogus-remotes")
553 queries
=broadcastAccFunction
<vector
<ComboAddress
> >(pleaseGetBogusRemotes
);
554 else if(req
->getvars
["name"]=="large-answer-remotes")
555 queries
=broadcastAccFunction
<vector
<ComboAddress
> >(pleaseGetLargeAnswerRemotes
);
556 else if(req
->getvars
["name"]=="timeouts")
557 queries
=broadcastAccFunction
<vector
<ComboAddress
> >(pleaseGetTimeouts
);
559 typedef map
<ComboAddress
,unsigned int,ComboAddress::addressOnlyLessThan
> counts_t
;
561 unsigned int total
=0;
562 for(const ComboAddress
& q
: queries
) {
567 typedef std::multimap
<int, ComboAddress
> rcounts_t
;
570 for(counts_t::const_iterator i
=counts
.begin(); i
!= counts
.end(); ++i
)
571 rcounts
.insert(make_pair(-i
->second
, i
->first
));
574 unsigned int tot
=0, totIncluded
=0;
575 for(const rcounts_t::value_type
& q
: rcounts
) {
576 totIncluded
-=q
.first
;
577 entries
.push_back(Json::array
{
578 -q
.first
, q
.second
.toString()
583 if(queries
.size() != totIncluded
) {
584 entries
.push_back(Json::array
{
585 (int)(queries
.size() - totIncluded
), ""
589 resp
->setBody(Json::object
{ { "entries", entries
} });
592 resp
->setErrorResult("Command '"+command
+"' not found", 404);
597 void AsyncServerNewConnectionMT(void *p
) {
598 AsyncServer
*server
= (AsyncServer
*)p
;
601 auto socket
= server
->accept(); // this is actually a shared_ptr
603 server
->d_asyncNewConnectionCallback(socket
);
605 } catch (NetworkError
&e
) {
606 // we're running in a shared process/thread, so can't just terminate/abort.
607 g_log
<<Logger::Warning
<<"Network error in web thread: "<<e
.what()<<endl
;
611 g_log
<<Logger::Warning
<<"Unknown error in web thread"<<endl
;
618 void AsyncServer::asyncWaitForConnections(FDMultiplexer
* fdm
, const newconnectioncb_t
& callback
)
620 d_asyncNewConnectionCallback
= callback
;
621 fdm
->addReadFD(d_server_socket
.getHandle(), boost::bind(&AsyncServer::newConnection
, this));
624 void AsyncServer::newConnection()
626 getMT()->makeThread(&AsyncServerNewConnectionMT
, this);
629 // This is an entry point from FDM, so it needs to catch everything.
630 void AsyncWebServer::serveConnection(std::shared_ptr
<Socket
> client
) const {
631 const string logprefix
= d_logprefix
+ to_string(getUniqueID()) + " ";
633 HttpRequest
req(logprefix
);
639 YaHTTP::AsyncRequestLoader yarl
;
640 yarl
.initialize(&req
);
641 client
->setNonBlocking();
645 while(!req
.complete
) {
646 int bytes
= arecvtcp(data
, 16384, client
.get(), true);
648 req
.complete
= yarl
.feed(data
);
655 } catch (YaHTTP::ParseError
&e
) {
656 // request stays incomplete
657 g_log
<<Logger::Warning
<<logprefix
<<"Unable to parse request: "<<e
.what()<<endl
;
660 if (d_loglevel
>= WebServer::LogLevel::None
) {
661 client
->getRemote(remote
);
664 logRequest(req
, remote
);
666 WebServer::handleRequest(req
, resp
);
671 logResponse(resp
, remote
, logprefix
);
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
;
678 catch(PDNSException
&e
) {
679 g_log
<<Logger::Error
<<logprefix
<<"Exception: "<<e
.reason
<<endl
;
681 catch(std::exception
&e
) {
682 if(strstr(e
.what(), "timeout")==0)
683 g_log
<<Logger::Error
<<logprefix
<<"STL Exception: "<<e
.what()<<endl
;
686 g_log
<<Logger::Error
<<logprefix
<<"Unknown exception"<<endl
;
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
;
694 void AsyncWebServer::go() {
697 auto server
= std::dynamic_pointer_cast
<AsyncServer
>(d_server
);
700 server
->asyncWaitForConnections(d_fdm
, boost::bind(&AsyncWebServer::serveConnection
, this, _1
));