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
);
300 if(req
->method
!= "GET")
301 throw HttpMethodNotAllowedException();
306 BOOST_FOREACH(const SyncRes::domainmap_t::value_type
& val
, *t_sstorage
->domainmap
) {
307 const SyncRes::AuthDomain
& zone
= val
.second
;
310 // id is the canonical lookup key, which doesn't actually match the name (in some cases)
311 string zoneId
= apiZoneNameToId(val
.first
);
312 Value
jzoneid(zoneId
.c_str(), doc
.GetAllocator()); // copy
313 jdi
.AddMember("id", jzoneid
, doc
.GetAllocator());
314 string url
= "/servers/localhost/zones/" + zoneId
;
315 Value
jurl(url
.c_str(), doc
.GetAllocator()); // copy
316 jdi
.AddMember("url", jurl
, doc
.GetAllocator());
317 jdi
.AddMember("name", val
.first
.c_str(), doc
.GetAllocator());
318 jdi
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
321 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
322 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
323 servers
.PushBack(value
, doc
.GetAllocator());
325 jdi
.AddMember("servers", servers
, doc
.GetAllocator());
326 bool rd
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
327 jdi
.AddMember("recursion_desired", rd
, doc
.GetAllocator());
328 doc
.PushBack(jdi
, doc
.GetAllocator());
333 static void apiServerZoneDetail(HttpRequest
* req
, HttpResponse
* resp
)
335 string zonename
= apiZoneIdToName(req
->path_parameters
["id"]);
338 SyncRes::domainmap_t::const_iterator iter
= t_sstorage
->domainmap
->find(zonename
);
339 if (iter
== t_sstorage
->domainmap
->end())
340 throw ApiException("Could not find domain '"+zonename
+"'");
342 if(req
->method
== "PUT" && !::arg().mustDo("experimental-api-readonly")) {
346 doDeleteZone(zonename
);
347 doCreateZone(document
);
348 reloadAuthAndForwards();
349 fillZone(stringFromJson(document
, "name"), resp
);
351 else if(req
->method
== "DELETE" && !::arg().mustDo("experimental-api-readonly")) {
352 if (!doDeleteZone(zonename
)) {
353 throw ApiException("Deleting domain failed");
356 reloadAuthAndForwards();
357 // empty body on success
359 resp
->status
= 204; // No Content: declare that the zone is gone now
360 } else if(req
->method
== "GET") {
361 fillZone(zonename
, resp
);
363 throw HttpMethodNotAllowedException();
367 static void apiServerSearchData(HttpRequest
* req
, HttpResponse
* resp
) {
368 if(req
->method
!= "GET")
369 throw HttpMethodNotAllowedException();
371 string q
= req
->parameters
["q"];
373 throw ApiException("Query q can't be blank");
378 BOOST_FOREACH(const SyncRes::domainmap_t::value_type
& val
, *t_sstorage
->domainmap
) {
379 string zoneId
= apiZoneNameToId(val
.first
);
380 if (pdns_ci_find(val
.first
, q
) != string::npos
) {
383 object
.AddMember("type", "zone", doc
.GetAllocator());
384 Value
jzoneId(zoneId
.c_str(), doc
.GetAllocator()); // copy
385 object
.AddMember("zone_id", jzoneId
, doc
.GetAllocator());
386 Value
jzoneName(val
.first
.c_str(), doc
.GetAllocator()); // copy
387 object
.AddMember("name", jzoneName
, doc
.GetAllocator());
388 doc
.PushBack(object
, doc
.GetAllocator());
391 // if zone name is an exact match, don't bother with returning all records/comments in it
392 if (val
.first
== q
) {
396 const SyncRes::AuthDomain
& zone
= val
.second
;
398 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type
& rr
, zone
.d_records
) {
399 if (pdns_ci_find(rr
.qname
, q
) == string::npos
&& pdns_ci_find(rr
.content
, q
) == string::npos
)
404 object
.AddMember("type", "record", doc
.GetAllocator());
405 Value
jzoneId(zoneId
.c_str(), doc
.GetAllocator()); // copy
406 object
.AddMember("zone_id", jzoneId
, doc
.GetAllocator());
407 Value
jzoneName(val
.first
.c_str(), doc
.GetAllocator()); // copy
408 object
.AddMember("zone_name", jzoneName
, doc
.GetAllocator());
409 Value
jname(rr
.qname
.c_str(), doc
.GetAllocator()); // copy
410 object
.AddMember("name", jname
, doc
.GetAllocator());
411 Value
jcontent(rr
.content
.c_str(), doc
.GetAllocator()); // copy
412 object
.AddMember("content", jcontent
, doc
.GetAllocator());
414 doc
.PushBack(object
, doc
.GetAllocator());
420 RecursorWebServer::RecursorWebServer(FDMultiplexer
* fdm
)
422 RecursorControlParser rcp
; // inits
424 d_ws
= new AsyncWebServer(fdm
, arg()["experimental-webserver-address"], arg().asNum("experimental-webserver-port"), arg()["experimental-webserver-password"]);
428 d_ws
->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat
, this, _1
, _2
));
429 d_ws
->registerApiHandler("/servers/localhost/config/allow-from", &apiServerConfigAllowFrom
);
430 d_ws
->registerApiHandler("/servers/localhost/config", &apiServerConfig
);
431 d_ws
->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog
);
432 d_ws
->registerApiHandler("/servers/localhost/search-data", &apiServerSearchData
);
433 d_ws
->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics
);
434 d_ws
->registerApiHandler("/servers/localhost/zones/<id>", &apiServerZoneDetail
);
435 d_ws
->registerApiHandler("/servers/localhost/zones", &apiServerZones
);
436 d_ws
->registerApiHandler("/servers/localhost", &apiServerDetail
);
437 d_ws
->registerApiHandler("/servers", &apiServer
);
442 void RecursorWebServer::jsonstat(HttpRequest
* req
, HttpResponse
*resp
)
446 if(req
->parameters
.count("command")) {
447 command
= req
->parameters
["command"];
448 req
->parameters
.erase("command");
451 map
<string
, string
> stats
;
452 if(command
== "domains") {
455 BOOST_FOREACH(const SyncRes::domainmap_t::value_type
& val
, *t_sstorage
->domainmap
) {
459 const SyncRes::AuthDomain
& zone
= val
.second
;
460 Value
zonename(val
.first
.c_str(), doc
.GetAllocator());
461 jzone
.AddMember("name", zonename
, doc
.GetAllocator());
462 jzone
.AddMember("type", "Zone", doc
.GetAllocator());
463 jzone
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
466 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
467 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
468 servers
.PushBack(value
, doc
.GetAllocator());
470 jzone
.AddMember("servers", servers
, doc
.GetAllocator());
471 bool rdbit
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
472 jzone
.AddMember("rdbit", rdbit
, doc
.GetAllocator());
474 doc
.PushBack(jzone
, doc
.GetAllocator());
479 else if(command
== "zone") {
480 string arg_zone
= req
->parameters
["zone"];
481 SyncRes::domainmap_t::const_iterator ret
= t_sstorage
->domainmap
->find(arg_zone
);
482 if (ret
!= t_sstorage
->domainmap
->end()) {
488 const SyncRes::AuthDomain
& zone
= ret
->second
;
489 Value
zonename(ret
->first
.c_str(), doc
.GetAllocator());
490 root
.AddMember("name", zonename
, doc
.GetAllocator());
491 root
.AddMember("type", "Zone", doc
.GetAllocator());
492 root
.AddMember("kind", zone
.d_servers
.empty() ? "Native" : "Forwarded", doc
.GetAllocator());
495 BOOST_FOREACH(const ComboAddress
& server
, zone
.d_servers
) {
496 Value
value(server
.toStringWithPort().c_str(), doc
.GetAllocator());
497 servers
.PushBack(value
, doc
.GetAllocator());
499 root
.AddMember("servers", servers
, doc
.GetAllocator());
500 bool rdbit
= zone
.d_servers
.empty() ? false : zone
.d_rdForward
;
501 root
.AddMember("rdbit", rdbit
, doc
.GetAllocator());
505 BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type
& rr
, zone
.d_records
) {
508 Value
jname(rr
.qname
.c_str(), doc
.GetAllocator()); // copy
509 object
.AddMember("name", jname
, doc
.GetAllocator());
510 Value
jtype(rr
.qtype
.getName().c_str(), doc
.GetAllocator()); // copy
511 object
.AddMember("type", jtype
, doc
.GetAllocator());
512 object
.AddMember("ttl", rr
.ttl
, doc
.GetAllocator());
513 object
.AddMember("priority", rr
.priority
, doc
.GetAllocator());
514 Value
jcontent(rr
.content
.c_str(), doc
.GetAllocator()); // copy
515 object
.AddMember("content", jcontent
, doc
.GetAllocator());
516 records
.PushBack(object
, doc
.GetAllocator());
518 root
.AddMember("records", records
, doc
.GetAllocator());
520 doc
.AddMember("zone", root
, doc
.GetAllocator());
524 resp
->body
= returnJsonError("Could not find domain '"+arg_zone
+"'");
528 else if(command
== "flush-cache") {
529 string canon
=toCanonic("", req
->parameters
["domain"]);
530 int count
= broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipeCache
, canon
));
531 count
+=broadcastAccFunction
<uint64_t>(boost::bind(pleaseWipeAndCountNegCache
, canon
));
532 stats
["number"]=lexical_cast
<string
>(count
);
533 resp
->body
= returnJsonObject(stats
);
536 else if(command
== "config") {
537 vector
<string
> items
= ::arg().list();
538 BOOST_FOREACH(const string
& var
, items
) {
539 stats
[var
] = ::arg()[var
];
541 resp
->body
= returnJsonObject(stats
);
544 else if(command
== "log-grep") {
545 // legacy parameter name hack
546 req
->parameters
["q"] = req
->parameters
["needle"];
547 apiServerSearchLog(req
, resp
);
550 else if(command
== "stats") {
551 stats
= getAllStatsMap();
552 resp
->body
= returnJsonObject(stats
);
556 resp
->body
= returnJsonError("Not found");
561 void AsyncServerNewConnectionMT(void *p
) {
562 AsyncServer
*server
= (AsyncServer
*)p
;
564 Socket
* socket
= server
->accept();
565 server
->d_asyncNewConnectionCallback(socket
);
567 } catch (NetworkError
&e
) {
568 // we're running in a shared process/thread, so can't just terminate/abort.
573 void AsyncServer::asyncWaitForConnections(FDMultiplexer
* fdm
, const newconnectioncb_t
& callback
)
575 d_asyncNewConnectionCallback
= callback
;
576 fdm
->addReadFD(d_server_socket
.getHandle(), boost::bind(&AsyncServer::newConnection
, this));
579 void AsyncServer::newConnection()
581 MT
->makeThread(&AsyncServerNewConnectionMT
, this);
585 void AsyncWebServer::serveConnection(Socket
*client
)
588 YaHTTP::AsyncRequestLoader
yarl(&req
);
589 client
->setNonBlocking();
593 while(!req
.complete
) {
595 int bytes
= arecvtcp(data
, 16384, client
, true);
597 req
.complete
= yarl
.feed(data
);
603 } catch (YaHTTP::ParseError
&e
) {
604 // request stays incomplete
607 HttpResponse resp
= handleRequest(req
);
612 // now send the reply
613 if (asendtcp(data
, client
) == -1 || data
.empty()) {
614 L
<<Logger::Error
<<"Failed sending reply to HTTP client"<<endl
;
618 void AsyncWebServer::go() {
621 ((AsyncServer
*)d_server
)->asyncWaitForConnections(d_fdm
, boost::bind(&AsyncWebServer::serveConnection
, this, _1
));