]> granicus.if.org Git - pdns/commitdiff
JSON API: provide flush-cache, notify, axfr-receive
authorChristian Hofstaedtler <christian@hofstaedtler.name>
Sun, 25 Jan 2015 21:16:08 +0000 (22:16 +0100)
committerChristian Hofstaedtler <christian@hofstaedtler.name>
Sun, 25 Jan 2015 21:50:45 +0000 (22:50 +0100)
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
pdns/json.cc
pdns/json.hh
pdns/ws-auth.cc
pdns/ws-recursor.cc
regression-tests.api/test_Servers.py
regression-tests.api/test_Zones.py

index c8f928bb23970a350b1b183686e360eb14bddd6c..cca8d32e94912689e4196ec792632065bb1d27be 100644 (file)
@@ -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
 -----------------------------------------------
index 9709076a92a67ab10e043ea485c168d6cfd5b2d7..8e24ca508bbbeeaea7f574d0d1f635649856f455 100644 (file)
@@ -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);
+}
index 863f8538d637913526cc77a72ab29c4ef28c9f05..774631d8e0bb2b4cd3ab1480010452594e3d316f 100644 (file)
@@ -28,6 +28,7 @@
 
 std::string returnJsonObject(const std::map<std::string, std::string>& 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);
index 4014791441f8b1110ac4a6af82584a4a437dc811..baa35d020ca530c88ac238121858c7beb198acf0 100644 (file)
@@ -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 <iomanip>
 #include "zoneparser-tng.hh"
+#include "common_startup.hh"
 
 #ifdef HAVE_CONFIG_H
 # include <config.h>
@@ -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<string, string> object;
-    object["number"]=lexical_cast<string>(number);
-    //cerr<<"Flushed cache for '"<<parameters["domain"]<<"', cleaned "<<number<<" records"<<endl;
-    resp->body = returnJsonObject(object);
-    resp->status = 200;
-    return;
-  }
-  else if(command == "pdns-control") {
-    if(req->method!="POST")
-      throw HttpMethodNotAllowedException();
-    // cout<<"post: "<<post<<endl;
-    rapidjson::Document document;
-    req->json(document);
-    // cout<<"Parameters: '"<<document["parameters"].GetString()<<"'\n";
-    vector<string> parameters;
-    stringtok(parameters, document["parameters"].GetString(), " \t");
-    
-    DynListener::g_funk_t* ptr=0;
-    if(!parameters.empty())
-      ptr = DynListener::getFunc(toUpper(parameters[0]));
-    map<string, string> 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<string, string> object;
+  object["count"] = lexical_cast<string>(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/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
       d_ws->registerApiHandler("/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
       d_ws->registerApiHandler("/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
       d_ws->registerApiHandler("/servers/localhost/zones/<id>/export", &apiServerZoneExport);
+      d_ws->registerApiHandler("/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
       d_ws->registerApiHandler("/servers/localhost/zones/<id>", &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));
index c6d982c6d3af95e8380dec7b59ced64a673a90dc..05ddb694a50605c840c4506d00e756b505989f7e 100644 (file)
@@ -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<uint64_t>(boost::bind(pleaseWipeCache, canon));
+  count += broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon));
+  map<string, string> object;
+  object["count"] = lexical_cast<string>(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<string, string> 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<uint64_t>(boost::bind(pleaseWipeCache, canon));
-    count+=broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon));
-    stats["number"]=lexical_cast<string>(count);
-    resp->body = returnJsonObject(stats);
-    return;
-  }
-  else if(command == "get-query-ring") {
+  if(command == "get-query-ring") {
     typedef pair<string,uint16_t> query_t;
     vector<query_t> 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<string> 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");
index 63e5511592eb3969024677d33451c40cdec20082..36ef586f98f7216ae536fbad68e136bc135c570c 100644 (file)
@@ -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.')
index eb2992ee4a1a6f75550ffdf27f3fe16825e6617f..b627d15fdcc0c6e43976ba8cafbad13f709c2d52 100644 (file)
@@ -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']