From a426cb895b3cc4ef6b9d75d9dee90114faa4e663 Mon Sep 17 00:00:00 2001 From: Christian Hofstaedtler Date: Sun, 25 Jan 2015 22:16:08 +0100 Subject: [PATCH] JSON API: provide flush-cache, notify, axfr-receive pdnscontrol used to send pdns/rec-control commands for those through the jsonstat command tunnel, but jsonstat (on Auth at least) doesn't do X-API-Key, so that functionality was broken. Also removes jsonstat from Auth completely and leaves only non-migrated commands in jsonstat in Recursor. --- docs/markdown/httpapi/api_spec.md | 9 +- pdns/json.cc | 32 +++++++ pdns/json.hh | 1 + pdns/ws-auth.cc | 114 ++++++++++++------------- pdns/ws-recursor.cc | 119 ++++----------------------- regression-tests.api/test_Servers.py | 13 +++ regression-tests.api/test_Zones.py | 19 +++++ 7 files changed, 140 insertions(+), 167 deletions(-) diff --git a/docs/markdown/httpapi/api_spec.md b/docs/markdown/httpapi/api_spec.md index c8f928bb2..cca8d32e9 100644 --- a/docs/markdown/httpapi/api_spec.md +++ b/docs/markdown/httpapi/api_spec.md @@ -49,6 +49,12 @@ For interactions that do not directly map onto CRUD, we use these: * GET: Query. Success reply: `200 OK` * PUT: Action/Execute. Success reply: `200 OK` +Action/Execute methods return a JSON body of this format: + + { + "message": "result message" + } + Authentication -------------- @@ -494,8 +500,6 @@ Not supported for recursors. Clients MUST NOT send a body. -**TODO**: Not yet implemented. - URL: /servers/:server\_id/zones/:zone\_id/axfr-retrieve ------------------------------------------------------- @@ -511,7 +515,6 @@ Not supported for recursors. Clients MUST NOT send a body. -**TODO**: Not yet implemented. URL: /servers/:server\_id/zones/:zone\_id/check ----------------------------------------------- diff --git a/pdns/json.cc b/pdns/json.cc index 9709076a9..8e24ca508 100644 --- a/pdns/json.cc +++ b/pdns/json.cc @@ -1,3 +1,24 @@ +/* + Copyright (C) 2002 - 2015 PowerDNS.COM BV + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2 + as published by the Free Software Foundation + + Additionally, the license of this program contains a special + exception which allows to distribute the program in binary form when + it is linked against OpenSSL. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + #include "json.hh" #include "namespaces.hh" #include "misc.hh" @@ -119,3 +140,14 @@ string returnJsonError(const string& error) doc.AddMember("error", jerror, doc.GetAllocator()); return makeStringFromDocument(doc); } + +/* success response */ +string returnJsonMessage(const string& message) +{ + Document doc; + doc.SetObject(); + Value jmessage; + jmessage.SetString(message.c_str()); + doc.AddMember("result", jmessage, doc.GetAllocator()); + return makeStringFromDocument(doc); +} diff --git a/pdns/json.hh b/pdns/json.hh index 863f8538d..774631d8e 100644 --- a/pdns/json.hh +++ b/pdns/json.hh @@ -28,6 +28,7 @@ std::string returnJsonObject(const std::map& items); std::string returnJsonError(const std::string& error); +std::string returnJsonMessage(const std::string& message); std::string makeStringFromDocument(const rapidjson::Document& doc); int intFromJson(const rapidjson::Value& container, const char* key); int intFromJson(const rapidjson::Value& container, const char* key, const int default_value); diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 401479144..baa35d020 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2002 - 2014 PowerDNS.COM BV + Copyright (C) 2002 - 2015 PowerDNS.COM BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 @@ -42,6 +42,7 @@ #include "dnsseckeeper.hh" #include #include "zoneparser-tng.hh" +#include "common_startup.hh" #ifdef HAVE_CONFIG_H # include @@ -852,6 +853,42 @@ static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) { } } +static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) { + string zonename = apiZoneIdToName(req->parameters["id"]); + + if(req->method != "PUT") + throw HttpMethodNotAllowedException(); + + UeberBackend B; + DomainInfo di; + if(!B.getDomainInfo(zonename, di)) + throw ApiException("Could not find domain '"+zonename+"'"); + + if(di.masters.empty()) + throw ApiException("Domain '"+zonename+"' is not a slave domain (or has no master defined)"); + + random_shuffle(di.masters.begin(), di.masters.end()); + Communicator.addSuckRequest(zonename, di.masters.front()); + resp->body = returnJsonMessage("Added retrieval request for '"+zonename+"' from master "+di.masters.front()); +} + +static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) { + string zonename = apiZoneIdToName(req->parameters["id"]); + + if(req->method != "PUT") + throw HttpMethodNotAllowedException(); + + UeberBackend B; + DomainInfo di; + if(!B.getDomainInfo(zonename, di)) + throw ApiException("Could not find domain '"+zonename+"'"); + + if(!Communicator.notifyDomain(zonename)) + throw ApiException("Failed to add to the queue - see server log"); + + resp->body = returnJsonMessage("Notification queued"); +} + static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) { if (rr.qtype.getCode() == QType::A) { uint32_t ip; @@ -1113,65 +1150,21 @@ static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) { resp->setBody(doc); } -void AuthWebServer::jsonstat(HttpRequest* req, HttpResponse* resp) -{ - string command; - - if(req->getvars.count("command")) { - command = req->getvars["command"]; - req->getvars.erase("command"); - } +void apiServerFlushCache(HttpRequest* req, HttpResponse* resp) { + if(req->method != "PUT") + throw HttpMethodNotAllowedException(); - if(command == "flush-cache") { - extern PacketCache PC; - int number; - if(req->getvars["domain"].empty()) - number = PC.purge(); - else - number = PC.purge(req->getvars["domain"]); - - map object; - object["number"]=lexical_cast(number); - //cerr<<"Flushed cache for '"<body = returnJsonObject(object); - resp->status = 200; - return; - } - else if(command == "pdns-control") { - if(req->method!="POST") - throw HttpMethodNotAllowedException(); - // cout<<"post: "<json(document); - // cout<<"Parameters: '"< parameters; - stringtok(parameters, document["parameters"].GetString(), " \t"); - - DynListener::g_funk_t* ptr=0; - if(!parameters.empty()) - ptr = DynListener::getFunc(toUpper(parameters[0])); - map m; - - if(ptr) { - resp->status = 200; - m["result"] = (*ptr)(parameters, 0); - } else { - resp->status = 404; - m["error"]="No such function "+toUpper(parameters[0]); - } - resp->body = returnJsonObject(m); - return; - } - else if(command=="log-grep") { - // legacy parameter name hack - req->getvars["q"] = req->getvars["needle"]; - apiServerSearchLog(req, resp); - return; - } + extern PacketCache PC; + int count; + if(req->getvars["domain"].empty()) + count = PC.purge(); + else + count = PC.purge(req->getvars["domain"]); - resp->body = returnJsonError("No or unknown command given"); - resp->status = 404; - return; + map object; + object["count"] = lexical_cast(count); + object["result"] = "Flushed cache."; + resp->body = returnJsonObject(object); } void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp) @@ -1215,18 +1208,19 @@ void AuthWebServer::webThread() try { if(::arg().mustDo("experimental-json-interface")) { d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig); + d_ws->registerApiHandler("/servers/localhost/flush-cache", &apiServerFlushCache); d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog); d_ws->registerApiHandler("/servers/localhost/search-data", &apiServerSearchData); d_ws->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics); + d_ws->registerApiHandler("/servers/localhost/zones//axfr-retrieve", &apiServerZoneAxfrRetrieve); d_ws->registerApiHandler("/servers/localhost/zones//cryptokeys/", &apiZoneCryptokeys); d_ws->registerApiHandler("/servers/localhost/zones//cryptokeys", &apiZoneCryptokeys); d_ws->registerApiHandler("/servers/localhost/zones//export", &apiServerZoneExport); + d_ws->registerApiHandler("/servers/localhost/zones//notify", &apiServerZoneNotify); d_ws->registerApiHandler("/servers/localhost/zones/", &apiServerZoneDetail); d_ws->registerApiHandler("/servers/localhost/zones", &apiServerZones); d_ws->registerApiHandler("/servers/localhost", &apiServerDetail); d_ws->registerApiHandler("/servers", &apiServer); - // legacy dispatch - d_ws->registerApiHandler("/jsonstat", boost::bind(&AuthWebServer::jsonstat, this, _1, _2)); } d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2)); d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2)); diff --git a/pdns/ws-recursor.cc b/pdns/ws-recursor.cc index c6d982c6d..05ddb694a 100644 --- a/pdns/ws-recursor.cc +++ b/pdns/ws-recursor.cc @@ -417,6 +417,19 @@ static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) { resp->setBody(doc); } +static void apiServerFlushCache(HttpRequest* req, HttpResponse* resp) { + if(req->method != "PUT") + throw HttpMethodNotAllowedException(); + + string canon = toCanonic("", req->getvars["domain"]); + int count = broadcastAccFunction(boost::bind(pleaseWipeCache, canon)); + count += broadcastAccFunction(boost::bind(pleaseWipeAndCountNegCache, canon)); + map object; + object["count"] = lexical_cast(count); + object["result"] = "Flushed cache."; + resp->body = returnJsonObject(object); +} + RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm) { RecursorControlParser rcp; // inits @@ -426,6 +439,7 @@ RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm) // legacy dispatch d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2)); + d_ws->registerApiHandler("/servers/localhost/flush-cache", &apiServerFlushCache); d_ws->registerApiHandler("/servers/localhost/config/allow-from", &apiServerConfigAllowFrom); d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig); d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog); @@ -449,90 +463,7 @@ void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp) } map stats; - if(command == "domains") { - Document doc; - doc.SetArray(); - BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) { - Value jzone; - jzone.SetObject(); - - const SyncRes::AuthDomain& zone = val.second; - Value zonename(val.first.c_str(), doc.GetAllocator()); - jzone.AddMember("name", zonename, doc.GetAllocator()); - jzone.AddMember("type", "Zone", doc.GetAllocator()); - jzone.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator()); - Value servers; - servers.SetArray(); - BOOST_FOREACH(const ComboAddress& server, zone.d_servers) { - Value value(server.toStringWithPort().c_str(), doc.GetAllocator()); - servers.PushBack(value, doc.GetAllocator()); - } - jzone.AddMember("servers", servers, doc.GetAllocator()); - bool rdbit = zone.d_servers.empty() ? false : zone.d_rdForward; - jzone.AddMember("rdbit", rdbit, doc.GetAllocator()); - - doc.PushBack(jzone, doc.GetAllocator()); - } - resp->setBody(doc); - return; - } - else if(command == "zone") { - string arg_zone = req->getvars["zone"]; - SyncRes::domainmap_t::const_iterator ret = t_sstorage->domainmap->find(arg_zone); - if (ret != t_sstorage->domainmap->end()) { - Document doc; - doc.SetObject(); - Value root; - root.SetObject(); - - const SyncRes::AuthDomain& zone = ret->second; - Value zonename(ret->first.c_str(), doc.GetAllocator()); - root.AddMember("name", zonename, doc.GetAllocator()); - root.AddMember("type", "Zone", doc.GetAllocator()); - root.AddMember("kind", zone.d_servers.empty() ? "Native" : "Forwarded", doc.GetAllocator()); - Value servers; - servers.SetArray(); - BOOST_FOREACH(const ComboAddress& server, zone.d_servers) { - Value value(server.toStringWithPort().c_str(), doc.GetAllocator()); - servers.PushBack(value, doc.GetAllocator()); - } - root.AddMember("servers", servers, doc.GetAllocator()); - bool rdbit = zone.d_servers.empty() ? false : zone.d_rdForward; - root.AddMember("rdbit", rdbit, doc.GetAllocator()); - - Value records; - records.SetArray(); - BOOST_FOREACH(const SyncRes::AuthDomain::records_t::value_type& rr, zone.d_records) { - Value object; - object.SetObject(); - Value jname(rr.qname.c_str(), doc.GetAllocator()); // copy - object.AddMember("name", jname, doc.GetAllocator()); - Value jtype(rr.qtype.getName().c_str(), doc.GetAllocator()); // copy - object.AddMember("type", jtype, doc.GetAllocator()); - object.AddMember("ttl", rr.ttl, doc.GetAllocator()); - Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy - object.AddMember("content", jcontent, doc.GetAllocator()); - records.PushBack(object, doc.GetAllocator()); - } - root.AddMember("records", records, doc.GetAllocator()); - - doc.AddMember("zone", root, doc.GetAllocator()); - resp->setBody(doc); - return; - } else { - resp->body = returnJsonError("Could not find domain '"+arg_zone+"'"); - return; - } - } - else if(command == "flush-cache") { - string canon=toCanonic("", req->getvars["domain"]); - int count = broadcastAccFunction(boost::bind(pleaseWipeCache, canon)); - count+=broadcastAccFunction(boost::bind(pleaseWipeAndCountNegCache, canon)); - stats["number"]=lexical_cast(count); - resp->body = returnJsonObject(stats); - return; - } - else if(command == "get-query-ring") { + if(command == "get-query-ring") { typedef pair query_t; vector queries; bool filter=!req->getvars["public-filtered"].empty(); @@ -640,26 +571,6 @@ void RecursorWebServer::jsonstat(HttpRequest* req, HttpResponse *resp) doc.AddMember("entries", entries, doc.GetAllocator()); resp->setBody(doc); return; - } - - else if(command == "config") { - vector items = ::arg().list(); - BOOST_FOREACH(const string& var, items) { - stats[var] = ::arg()[var]; - } - resp->body = returnJsonObject(stats); - return; - } - else if(command == "log-grep") { - // legacy parameter name hack - req->getvars["q"] = req->getvars["needle"]; - apiServerSearchLog(req, resp); - return; - } - else if(command == "stats") { - stats = getAllStatsMap(); - resp->body = returnJsonObject(stats); - return; } else { resp->status = 404; resp->body = returnJsonError("Command '"+command+"' not found"); diff --git a/regression-tests.api/test_Servers.py b/regression-tests.api/test_Servers.py index 63e551159..36ef586f9 100644 --- a/regression-tests.api/test_Servers.py +++ b/regression-tests.api/test_Servers.py @@ -41,3 +41,16 @@ class Servers(ApiTestCase): self.assert_success_json(r) data = dict([(r['name'], r['value']) for r in r.json()]) self.assertIn('uptime', data) + + def test_flush_cache(self): + r = self.session.put(self.url("/servers/localhost/flush-cache?domain=example.org.")) + self.assert_success_json(r) + data = r.json() + self.assertIn('count', data) + + def test_flush_complete_cache(self): + r = self.session.put(self.url("/servers/localhost/flush-cache")) + self.assert_success_json(r) + data = r.json() + self.assertIn('count', data) + self.assertEqual(data['result'], 'Flushed cache.') diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index eb2992ee4..b627d15fd 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -178,6 +178,25 @@ class AuthZones(ApiTestCase): r = self.session.delete(self.url("/servers/localhost/zones/" + data['id'])) r.raise_for_status() + def test_retrieve_slave_zone(self): + payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2']) + print "payload:", payload + print "data:", data + r = self.session.put(self.url("/servers/localhost/zones/" + data['id'] + "/axfr-retrieve")) + data = r.json() + print "status for axfr-retrieve:", data + self.assertEqual(data['result'], u'Added retrieval request for \'' + payload['name'] + + '\' from master 127.0.0.2') + + def test_notify_master_zone(self): + payload, data = self.create_zone(kind='Master') + print "payload:", payload + print "data:", data + r = self.session.put(self.url("/servers/localhost/zones/" + data['id'] + "/notify")) + data = r.json() + print "status for notify:", data + self.assertEqual(data['result'], 'Notification queued') + def test_get_zone_with_symbols(self): payload, data = self.create_zone(name='foo/bar.'+unique_zone_name()) name = payload['name'] -- 2.40.0