2 PowerDNS Versatile Database Driven Nameserver
3 Copyright (C) 2003 - 2014 PowerDNS.COM BV
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License version 2
7 as published by the Free Software Foundation
9 Additionally, the license of this program contains a special
10 exception which allows to distribute the program in binary form when
11 it is linked against OpenSSL.
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 St, Fifth Floor, Boston, MA 02110-1301 USA
22 #include "ws-recursor.hh"
24 #include <boost/foreach.hpp>
26 #include "namespaces.hh"
29 #include "rec_channel.hh"
30 #include "arguments.hh"
33 #include "rapidjson/document.h"
34 #include "rapidjson/stringbuffer.h"
35 #include "rapidjson/writer.h"
36 #include "webserver.hh"
40 extern __thread FDMultiplexer
* t_fdm
;
42 using namespace rapidjson
;
44 void productServerStatisticsFetch(map
<string
,string
>& out
)
46 map
<string
,string
> stats
= getAllStatsMap();
50 static void apiWriteConfigFile(const string
& filebasename
, const string
& content
)
52 if (::arg()["experimental-api-config-dir"].empty()) {
53 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
56 string filename
= ::arg()["experimental-api-config-dir"] + "/" + filebasename
+ ".conf";
57 ofstream
ofconf(filename
.c_str());
59 throw ApiException("Could not open config fragment file '"+filename
+"' for writing: "+stringerror());
61 ofconf
<< "# Generated by pdns-recursor REST API, DO NOT EDIT" << endl
;
62 ofconf
<< content
<< endl
;
66 static void apiServerConfigAllowFrom(HttpRequest
* req
, HttpResponse
* resp
)
68 if (req
->method
== "PUT" && !::arg().mustDo("experimental-api-readonly")) {
71 const Value
&jlist
= document
["value"];
73 if (!document
.IsObject()) {
74 throw ApiException("Supplied JSON must be an object");
77 if (!jlist
.IsArray()) {
78 throw ApiException("'value' must be an array");
81 for (SizeType i
= 0; i
< jlist
.Size(); ++i
) {
83 Netmask(jlist
[i
].GetString());
84 } catch (NetmaskException
&e
) {
85 throw ApiException(e
.reason
);
91 // Clear allow-from-file if set, so our changes take effect
92 ss
<< "allow-from-file=" << endl
;
94 // Clear allow-from, and provide a "parent" value
95 ss
<< "allow-from=" << endl
;
96 for (SizeType i
= 0; i
< jlist
.Size(); ++i
) {
97 ss
<< "allow-from+=" << jlist
[i
].GetString() << endl
;
100 apiWriteConfigFile("allow-from", ss
.str());
104 // fall through to GET
105 } else if (req
->method
!= "GET") {
106 throw HttpMethodNotAllowedException();
109 // Return currently configured ACLs
111 document
.SetObject();
116 vector
<string
> entries
;
117 t_allowFrom
->toStringVector(&entries
);
119 BOOST_FOREACH(const string
& entry
, entries
) {
120 Value
jentry(entry
.c_str(), document
.GetAllocator()); // copy
121 jlist
.PushBack(jentry
, document
.GetAllocator());
124 document
.AddMember("name", "allow-from", document
.GetAllocator());
125 document
.AddMember("value", jlist
, document
.GetAllocator());
127 resp
->setBody(document
);
130 static void fillZone(const string
& zonename
, HttpResponse
* resp
)
132 SyncRes::domainmap_t::const_iterator iter
= t_sstorage
->domainmap
->find(zonename
);
133 if (iter
== t_sstorage
->domainmap
->end())
134 throw ApiException("Could not find domain '"+zonename
+"'");
139 const SyncRes::AuthDomain
& zone
= iter
->second
;
141 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
142 string zoneId
= apiZoneNameToId(iter
->first
);
143 Value
jzoneid(zoneId
.c_str(), doc
.GetAllocator()); // copy
144 doc
.AddMember("id", jzoneid
, doc
.GetAllocator());
145 string url
= "/servers/localhost/zones/" + zoneId
;
146 Value
jurl(url
.c_str(), doc
.GetAllocator()); // copy
147 doc
.AddMember("url", jurl
, doc
.GetAllocator());
148 doc
.AddMember("name", iter
->first
.c_str(), doc
.GetAllocator());
149 doc
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
152 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
153 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
154 servers
.PushBack(value
, doc
.GetAllocator());
156 doc
.AddMember("servers", servers
, doc
.GetAllocator());
157 bool rd
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
158 doc
.AddMember("recursion_desired", rd
, doc
.GetAllocator());
162 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type
& rr
, zone
.d_records
) {
165 Value
jname(rr
.qname
.c_str(), doc
.GetAllocator()); // copy
166 object
.AddMember("name", jname
, doc
.GetAllocator());
167 Value
jtype(rr
.qtype
.getName().c_str(), doc
.GetAllocator()); // copy
168 object
.AddMember("type", jtype
, doc
.GetAllocator());
169 object
.AddMember("ttl", rr
.ttl
, doc
.GetAllocator());
170 object
.AddMember("priority", rr
.priority
, doc
.GetAllocator());
171 Value
jcontent(rr
.content
.c_str(), doc
.GetAllocator()); // copy
172 object
.AddMember("content", jcontent
, doc
.GetAllocator());
173 records
.PushBack(object
, doc
.GetAllocator());
175 doc
.AddMember("records", records
, doc
.GetAllocator());
180 static void doCreateZone(const Value
& document
)
182 if (::arg()["experimental-api-config-dir"].empty()) {
183 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
186 string zonename
= stringFromJson(document
, "name");
187 // TODO: better validation of zonename - apiZoneNameToId takes care of escaping / however
189 throw ApiException("Zone name empty");
191 if (zonename
[zonename
.size()-1] != '.') {
195 string singleIPTarget
= stringFromJson(document
, "single_target_ip", "");
196 string kind
= toUpper(stringFromJson(document
, "kind"));
197 bool rd
= boolFromJson(document
, "recursion_desired");
198 string confbasename
= "zone-" + apiZoneNameToId(zonename
);
200 if (kind
== "NATIVE") {
202 throw ApiException("kind=Native and recursion_desired are mutually exclusive");
203 if(!singleIPTarget
.empty()) {
205 ComboAddress
rem(singleIPTarget
);
206 if(rem
.sin4
.sin_family
!= AF_INET
)
207 throw ApiException("");
208 singleIPTarget
= rem
.toString();
211 throw ApiException("Single IP target '"+singleIPTarget
+"' is invalid");
214 string zonefilename
= ::arg()["experimental-api-config-dir"] + "/" + confbasename
+ ".zone";
215 ofstream
ofzone(zonefilename
.c_str());
217 throw ApiException("Could not open '"+zonefilename
+"' for writing: "+stringerror());
219 ofzone
<< "; Generated by pdns-recursor REST API, DO NOT EDIT" << endl
;
220 ofzone
<< zonename
<< "\tIN\tSOA\tlocal.zone.\thostmaster."<<zonename
<<" 1 1 1 1 1" << endl
;
221 if(!singleIPTarget
.empty()) {
222 ofzone
<<zonename
<< "\t3600\tIN\tA\t"<<singleIPTarget
<<endl
;
223 ofzone
<<"*."<<zonename
<< "\t3600\tIN\tA\t"<<singleIPTarget
<<endl
;
227 apiWriteConfigFile(confbasename
, "auth-zones+=" + zonename
+ "=" + zonefilename
);
228 } else if (kind
== "FORWARDED") {
229 const Value
&servers
= document
["servers"];
230 if (kind
== "FORWARDED" && (!servers
.IsArray() || servers
.Size() == 0))
231 throw ApiException("Need at least one upstream server when forwarding");
234 if (servers
.IsArray()) {
235 for (SizeType i
= 0; i
< servers
.Size(); ++i
) {
236 if (!serverlist
.empty()) {
239 serverlist
+= servers
[i
].GetString();
244 apiWriteConfigFile(confbasename
, "forward-zones-recurse+=" + zonename
+ "=" + serverlist
);
246 apiWriteConfigFile(confbasename
, "forward-zones+=" + zonename
+ "=" + serverlist
);
249 throw ApiException("invalid kind");
253 static bool doDeleteZone(const string
& zonename
)
255 if (::arg()["experimental-api-config-dir"].empty()) {
256 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
261 // this one must exist
262 filename
= ::arg()["experimental-api-config-dir"] + "/zone-" + apiZoneNameToId(zonename
) + ".conf";
263 if (unlink(filename
.c_str()) != 0) {
267 // .zone file is optional
268 filename
= ::arg()["experimental-api-config-dir"] + "/zone-" + apiZoneNameToId(zonename
) + ".zone";
269 unlink(filename
.c_str());
274 static void apiServerZones(HttpRequest
* req
, HttpResponse
* resp
)
276 if (req
->method
== "POST" && !::arg().mustDo("experimental-api-readonly")) {
277 if (::arg()["experimental-api-config-dir"].empty()) {
278 throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
284 string zonename
= stringFromJson(document
, "name");
285 if (zonename
[zonename
.size()-1] != '.') {
289 SyncRes::domainmap_t::const_iterator iter
= t_sstorage
->domainmap
->find(zonename
);
290 if (iter
!= t_sstorage
->domainmap
->end())
291 throw ApiException("Zone already exists");
293 doCreateZone(document
);
294 reloadAuthAndForwards();
295 fillZone(zonename
, resp
);
299 if(req
->method
!= "GET")
300 throw HttpMethodNotAllowedException();
305 BOOST_FOREACH(const SyncRes::domainmap_t::value_type
& val
, *t_sstorage
->domainmap
) {
306 const SyncRes::AuthDomain
& zone
= val
.second
;
309 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
310 string zoneId
= apiZoneNameToId(val
.first
);
311 Value
jzoneid(zoneId
.c_str(), doc
.GetAllocator()); // copy
312 jdi
.AddMember("id", jzoneid
, doc
.GetAllocator());
313 string url
= "/servers/localhost/zones/" + zoneId
;
314 Value
jurl(url
.c_str(), doc
.GetAllocator()); // copy
315 jdi
.AddMember("url", jurl
, doc
.GetAllocator());
316 jdi
.AddMember("name", val
.first
.c_str(), doc
.GetAllocator());
317 jdi
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
320 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
321 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
322 servers
.PushBack(value
, doc
.GetAllocator());
324 jdi
.AddMember("servers", servers
, doc
.GetAllocator());
325 bool rd
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
326 jdi
.AddMember("recursion_desired", rd
, doc
.GetAllocator());
327 doc
.PushBack(jdi
, doc
.GetAllocator());
332 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
)
334 string zonename
= apiZoneIdToName(req
->parameters
["id"]);
337 SyncRes::domainmap_t::const_iterator iter
= t_sstorage
->domainmap
->find(zonename
);
338 if (iter
== t_sstorage
->domainmap
->end())
339 throw ApiException("Could not find domain '"+zonename
+"'");
341 if(req
->method
== "PUT" && !::arg().mustDo("experimental-api-readonly")) {
345 doDeleteZone(zonename
);
346 doCreateZone(document
);
347 reloadAuthAndForwards();
348 fillZone(stringFromJson(document
, "name"), resp
);
350 else if(req
->method
== "DELETE" && !::arg().mustDo("experimental-api-readonly")) {
351 if (!doDeleteZone(zonename
)) {
352 throw ApiException("Deleting domain failed");
355 reloadAuthAndForwards();
356 // empty body on success
358 resp
->status
= 204; // No Content: declare that the zone is gone now
359 } else if(req
->method
== "GET") {
360 fillZone(zonename
, resp
);
362 throw HttpMethodNotAllowedException();
366 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
367 if(req
->method
!= "GET")
368 throw HttpMethodNotAllowedException();
370 string q
= req
->getvars
["q"];
372 throw ApiException("Query q can't be blank");
377 BOOST_FOREACH(const SyncRes::domainmap_t::value_type
& val
, *t_sstorage
->domainmap
) {
378 string zoneId
= apiZoneNameToId(val
.first
);
379 if (pdns_ci_find(val
.first
, q
) != string::npos
) {
382 object
.AddMember("type", "zone", doc
.GetAllocator());
383 Value
jzoneId(zoneId
.c_str(), doc
.GetAllocator()); // copy
384 object
.AddMember("zone_id", jzoneId
, doc
.GetAllocator());
385 Value
jzoneName(val
.first
.c_str(), doc
.GetAllocator()); // copy
386 object
.AddMember("name", jzoneName
, doc
.GetAllocator());
387 doc
.PushBack(object
, doc
.GetAllocator());
390 // if zone name is an exact match, don't bother with returning all records/comments in it
391 if (val
.first
== q
) {
395 const SyncRes::AuthDomain
& zone
= val
.second
;
397 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type
& rr
, zone
.d_records
) {
398 if (pdns_ci_find(rr
.qname
, q
) == string::npos
&& pdns_ci_find(rr
.content
, q
) == string::npos
)
403 object
.AddMember("type", "record", doc
.GetAllocator());
404 Value
jzoneId(zoneId
.c_str(), doc
.GetAllocator()); // copy
405 object
.AddMember("zone_id", jzoneId
, doc
.GetAllocator());
406 Value
jzoneName(val
.first
.c_str(), doc
.GetAllocator()); // copy
407 object
.AddMember("zone_name", jzoneName
, doc
.GetAllocator());
408 Value
jname(rr
.qname
.c_str(), doc
.GetAllocator()); // copy
409 object
.AddMember("name", jname
, doc
.GetAllocator());
410 Value
jcontent(rr
.content
.c_str(), doc
.GetAllocator()); // copy
411 object
.AddMember("content", jcontent
, doc
.GetAllocator());
413 doc
.PushBack(object
, doc
.GetAllocator());
419 RecursorWebServer::RecursorWebServer(FDMultiplexer
* fdm
)
421 RecursorControlParser rcp
; // inits
423 d_ws
= new AsyncWebServer(fdm
, arg()["experimental-webserver-address"], arg().asNum("experimental-webserver-port"), arg()["experimental-webserver-password"]);
427 d_ws
->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat
, this, _1
, _2
));
428 d_ws
->registerApiHandler("/servers/localhost/config/allow-from", &apiServerConfigAllowFrom
);
429 d_ws
->registerApiHandler("/servers/localhost/config", &apiServerConfig
);
430 d_ws
->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog
);
431 d_ws
->registerApiHandler("/servers/localhost/search-data", &apiServerSearchData
);
432 d_ws
->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics
);
433 d_ws
->registerApiHandler("/servers/localhost/zones/<id>", &apiServerZoneDetail
);
434 d_ws
->registerApiHandler("/servers/localhost/zones", &apiServerZones
);
435 d_ws
->registerApiHandler("/servers/localhost", &apiServerDetail
);
436 d_ws
->registerApiHandler("/servers", &apiServer
);
441 void RecursorWebServer::jsonstat(HttpRequest
* req
, HttpResponse
*resp
)
445 if(req
->getvars
.count("command")) {
446 command
= req
->getvars
["command"];
447 req
->getvars
.erase("command");
450 map
<string
, string
> stats
;
451 if(command
== "domains") {
454 BOOST_FOREACH(const SyncRes::domainmap_t::value_type
& val
, *t_sstorage
->domainmap
) {
458 const SyncRes::AuthDomain
& zone
= val
.second
;
459 Value
zonename(val
.first
.c_str(), doc
.GetAllocator());
460 jzone
.AddMember("name", zonename
, doc
.GetAllocator());
461 jzone
.AddMember("type", "Zone", doc
.GetAllocator());
462 jzone
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
465 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
466 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
467 servers
.PushBack(value
, doc
.GetAllocator());
469 jzone
.AddMember("servers", servers
, doc
.GetAllocator());
470 bool rdbit
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
471 jzone
.AddMember("rdbit", rdbit
, doc
.GetAllocator());
473 doc
.PushBack(jzone
, doc
.GetAllocator());
478 else if(command
== "zone") {
479 string arg_zone
= req
->getvars
["zone"];
480 SyncRes::domainmap_t::const_iterator ret
= t_sstorage
->domainmap
->find(arg_zone
);
481 if (ret
!= t_sstorage
->domainmap
->end()) {
487 const SyncRes::AuthDomain
& zone
= ret
->second
;
488 Value
zonename(ret
->first
.c_str(), doc
.GetAllocator());
489 root
.AddMember("name", zonename
, doc
.GetAllocator());
490 root
.AddMember("type", "Zone", doc
.GetAllocator());
491 root
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
494 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
495 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
496 servers
.PushBack(value
, doc
.GetAllocator());
498 root
.AddMember("servers", servers
, doc
.GetAllocator());
499 bool rdbit
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
500 root
.AddMember("rdbit", rdbit
, doc
.GetAllocator());
504 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type
& rr
, zone
.d_records
) {
507 Value
jname(rr
.qname
.c_str(), doc
.GetAllocator()); // copy
508 object
.AddMember("name", jname
, doc
.GetAllocator());
509 Value
jtype(rr
.qtype
.getName().c_str(), doc
.GetAllocator()); // copy
510 object
.AddMember("type", jtype
, doc
.GetAllocator());
511 object
.AddMember("ttl", rr
.ttl
, doc
.GetAllocator());
512 object
.AddMember("priority", rr
.priority
, doc
.GetAllocator());
513 Value
jcontent(rr
.content
.c_str(), doc
.GetAllocator()); // copy
514 object
.AddMember("content", jcontent
, doc
.GetAllocator());
515 records
.PushBack(object
, doc
.GetAllocator());
517 root
.AddMember("records", records
, doc
.GetAllocator());
519 doc
.AddMember("zone", root
, doc
.GetAllocator());
523 resp
->body
= returnJsonError("Could not find domain '"+arg_zone
+"'");
527 else if(command
== "flush-cache") {
528 string canon
=toCanonic("", req
->getvars
["domain"]);
529 int count
= broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipeCache
, canon
));
530 count
+=broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipeAndCountNegCache
, canon
));
531 stats
["number"]=lexical_cast
<string
>(count
);
532 resp
->body
= returnJsonObject(stats
);
535 else if(command
== "config") {
536 vector
<string
> items
= ::arg().list();
537 BOOST_FOREACH(const string
& var
, items
) {
538 stats
[var
] = ::arg()[var
];
540 resp
->body
= returnJsonObject(stats
);
543 else if(command
== "log-grep") {
544 // legacy parameter name hack
545 req
->getvars
["q"] = req
->getvars
["needle"];
546 apiServerSearchLog(req
, resp
);
549 else if(command
== "stats") {
550 stats
= getAllStatsMap();
551 resp
->body
= returnJsonObject(stats
);
555 resp
->body
= returnJsonError("Not found");
560 void AsyncServerNewConnectionMT(void *p
) {
561 AsyncServer
*server
= (AsyncServer
*)p
;
563 Socket
* socket
= server
->accept();
564 server
->d_asyncNewConnectionCallback(socket
);
566 } catch (NetworkError
&e
) {
567 // we're running in a shared process/thread, so can't just terminate/abort.
572 void AsyncServer::asyncWaitForConnections(FDMultiplexer
* fdm
, const newconnectioncb_t
& callback
)
574 d_asyncNewConnectionCallback
= callback
;
575 fdm
->addReadFD(d_server_socket
.getHandle(), boost::bind(&AsyncServer::newConnection
, this));
578 void AsyncServer::newConnection()
580 MT
->makeThread(&AsyncServerNewConnectionMT
, this);
584 void AsyncWebServer::serveConnection(Socket
*client
)
587 YaHTTP::AsyncRequestLoader yarl
;
588 yarl
.initialize(&req
);
589 client
->setNonBlocking();
593 while(!req
.complete
) {
595 int bytes
= arecvtcp(data
, 16384, client
, true);
597 req
.complete
= yarl
.feed(data
);
604 } catch (YaHTTP::ParseError
&e
) {
605 // request stays incomplete
608 HttpResponse resp
= handleRequest(req
);
613 // now send the reply
614 if (asendtcp(data
, client
) == -1 || data
.empty()) {
615 L
<<Logger::Error
<<"Failed sending reply to HTTP client"<<endl
;
619 void AsyncWebServer::go() {
622 ((AsyncServer
*)d_server
)->asyncWaitForConnections(d_fdm
, boost::bind(&AsyncWebServer::serveConnection
, this, _1
));