resp->setBody(document);
}
+static void fillZone(const string& zonename, HttpResponse* resp)
+{
+ SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
+ if (iter == t_sstorage->domainmap->end())
+ throw ApiException("Could not find domain '"+zonename+"'");
+
+ Document doc;
+ doc.SetObject();
+
+ const SyncRes::AuthDomain& zone = iter->second;
+
+ // id is the canonical lookup key, which doesn't actually match the name (in some cases)
+ string zoneId = apiZoneNameToId(iter->first);
+ doc.AddMember("id", zoneId.c_str(), doc.GetAllocator());
+ string url = "/servers/localhost/zones/" + zoneId;
+ Value jurl(url.c_str(), doc.GetAllocator()); // copy
+ doc.AddMember("url", jurl, doc.GetAllocator());
+ doc.AddMember("name", iter->first.c_str(), doc.GetAllocator());
+ doc.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());
+ }
+ doc.AddMember("servers", servers, doc.GetAllocator());
+ bool rd = zone.d_servers.empty() ? false : zone.d_rdForward;
+ doc.AddMember("recursion_desired", rd, 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());
+ object.AddMember("priority", rr.priority, doc.GetAllocator());
+ Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
+ object.AddMember("content", jcontent, doc.GetAllocator());
+ records.PushBack(object, doc.GetAllocator());
+ }
+ doc.AddMember("records", records, doc.GetAllocator());
+
+ resp->setBody(doc);
+}
+
+static void doCreateZone(const Value& document)
+{
+ if (::arg()["experimental-api-config-dir"].empty()) {
+ throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
+ }
+
+ string zonename = stringFromJson(document, "name");
+ // TODO: better validation of zonename
+ if(zonename.empty())
+ throw ApiException("Zone name empty");
+
+ if (zonename[zonename.size()-1] != '.') {
+ zonename += ".";
+ }
+
+ string kind = toUpper(stringFromJson(document, "kind"));
+ bool rd = boolFromJson(document, "recursion_desired");
+ string confbasename = "zone-" + apiZoneNameToId(zonename);
+
+ if (kind == "NATIVE") {
+ if (rd)
+ throw ApiException("kind=Native and recursion_desired are mutually exclusive");
+
+ string zonefilename = ::arg()["experimental-api-config-dir"] + "/" + confbasename + ".zone";
+ ofstream ofzone(zonefilename.c_str());
+ if (!ofzone) {
+ throw ApiException("Could not open '"+zonefilename+"' for writing: "+stringerror());
+ }
+ ofzone << "; Generated by pdns-recursor REST API, DO NOT EDIT" << endl;
+ ofzone << zonename << "\tIN\tSOA\tlocal.zone.\thostmaster."<<zonename<<" 1 1 1 1 1" << endl;
+ ofzone.close();
+
+ apiWriteConfigFile(confbasename, "auth-zones+=" + zonename + "=" + zonefilename);
+ } else if (kind == "FORWARDED") {
+ const Value &servers = document["servers"];
+ if (kind == "FORWARDED" && (!servers.IsArray() || servers.Size() == 0))
+ throw ApiException("Need at least one upstream server when forwarding");
+
+ string serverlist;
+ if (servers.IsArray()) {
+ for (SizeType i = 0; i < servers.Size(); ++i) {
+ if (!serverlist.empty()) {
+ serverlist += ";";
+ }
+ serverlist += servers[i].GetString();
+ }
+ }
+
+ if (rd) {
+ apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename + "=" + serverlist);
+ } else {
+ apiWriteConfigFile(confbasename, "forward-zones+=" + zonename + "=" + serverlist);
+ }
+ } else {
+ throw ApiException("invalid kind");
+ }
+}
+
+static bool doDeleteZone(const string& zonename)
+{
+ if (::arg()["experimental-api-config-dir"].empty()) {
+ throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
+ }
+
+ string filename;
+
+ // this one must exist
+ filename = ::arg()["experimental-api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf";
+ if (unlink(filename.c_str()) != 0) {
+ return false;
+ }
+
+ // .zone file is optional
+ filename = ::arg()["experimental-api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".zone";
+ unlink(filename.c_str());
+
+ return true;
+}
+
+static void apiServerZones(HttpRequest* req, HttpResponse* resp)
+{
+ if (req->method == "POST") {
+ if (::arg()["experimental-api-config-dir"].empty()) {
+ throw ApiException("Config Option \"experimental-api-config-dir\" must be set");
+ }
+
+ Document document;
+ req->json(document);
+
+ string zonename = stringFromJson(document, "name");
+ if (zonename[zonename.size()-1] != '.') {
+ zonename += ".";
+ }
+
+ SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
+ if (iter != t_sstorage->domainmap->end())
+ throw ApiException("Zone already exists");
+
+ doCreateZone(document);
+ reloadAuthAndForwards();
+ fillZone(zonename, resp);
+ return;
+ }
+
+ if(req->method != "GET")
+ throw HttpMethodNotAllowedException();
+
+ Document doc;
+ doc.SetArray();
+
+ BOOST_FOREACH(const SyncRes::domainmap_t::value_type& val, *t_sstorage->domainmap) {
+ const SyncRes::AuthDomain& zone = val.second;
+ Value jdi;
+ jdi.SetObject();
+ // id is the canonical lookup key, which doesn't actually match the name (in some cases)
+ string zoneId = apiZoneNameToId(val.first);
+ jdi.AddMember("id", zoneId.c_str(), doc.GetAllocator());
+ string url = "/servers/localhost/zones/" + zoneId;
+ Value jurl(url.c_str(), doc.GetAllocator()); // copy
+ jdi.AddMember("url", jurl, doc.GetAllocator());
+ jdi.AddMember("name", val.first.c_str(), doc.GetAllocator());
+ jdi.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());
+ }
+ jdi.AddMember("servers", servers, doc.GetAllocator());
+ bool rd = zone.d_servers.empty() ? false : zone.d_rdForward;
+ jdi.AddMember("recursion_desired", rd, doc.GetAllocator());
+ doc.PushBack(jdi, doc.GetAllocator());
+ }
+ resp->setBody(doc);
+}
+
+static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
+{
+ string zonename = apiZoneIdToName(req->path_parameters["id"]);
+ zonename += ".";
+
+ SyncRes::domainmap_t::const_iterator iter = t_sstorage->domainmap->find(zonename);
+ if (iter == t_sstorage->domainmap->end())
+ throw ApiException("Could not find domain '"+zonename+"'");
+
+ if(req->method == "PUT") {
+ Document document;
+ req->json(document);
+
+ doDeleteZone(zonename);
+ doCreateZone(document);
+ reloadAuthAndForwards();
+ fillZone(zonename, resp);
+ }
+ else if(req->method == "DELETE") {
+ if (!doDeleteZone(zonename)) {
+ throw ApiException("Deleting domain failed");
+ }
+
+ reloadAuthAndForwards();
+ // empty body on success
+ resp->body = "";
+ } else if(req->method == "GET") {
+ fillZone(zonename, resp);
+ } else {
+ throw HttpMethodNotAllowedException();
+ }
+}
+
RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
{
RecursorControlParser rcp; // inits
d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig);
d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog);
d_ws->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics);
+ 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);
import json
import requests
import unittest
-from test_helper import ApiTestCase, unique_zone_name, isRecursor
+from test_helper import ApiTestCase, unique_zone_name, isAuth, isRecursor
-@unittest.skipIf(isRecursor(), "Not implemented yet")
-class Servers(ApiTestCase):
+class Zones(ApiTestCase):
def test_ListZones(self):
r = self.session.get(self.url("/servers/localhost/zones"))
self.assertSuccessJson(r)
domains = r.json()
- example_com = [domain for domain in domains if domain['name'] == u'example.com']
+ example_com = [domain for domain in domains if domain['name'] in ('example.com', 'example.com.')]
self.assertEquals(len(example_com), 1)
example_com = example_com[0]
- for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
- self.assertIn(k, example_com)
+ required_fields = ['id', 'url', 'name', 'kind']
+ if isAuth():
+ required_fields = required_fields + ['masters', 'last_check', 'notified_serial', 'serial']
+ elif isRecursor():
+ required_fields = required_fields + ['recursion_desired', 'servers']
+ for field in required_fields:
+ self.assertIn(field, example_com)
+
+
+@unittest.skipIf(not isAuth(), "Not applicable")
+class AuthZones(ApiTestCase):
def create_zone(self, name=None, nameservers=None):
if name is None:
data=json.dumps(payload),
headers={'content-type': 'application/json'})
self.assertSuccessJson(r)
+
+
+@unittest.skipIf(not isRecursor(), "Not applicable")
+class RecursorZones(ApiTestCase):
+
+ def test_CreateAuthZone(self):
+ payload = {
+ 'name': unique_zone_name(),
+ 'kind': 'Native',
+ 'recursion_desired': False
+ }
+ r = self.session.post(
+ self.url("/servers/localhost/zones"),
+ data=json.dumps(payload),
+ headers={'content-type': 'application/json'})
+ self.assertSuccessJson(r)
+ data = r.json()
+ # return values are normalized
+ payload['name'] += '.'
+ for k in payload.keys():
+ self.assertEquals(data[k], payload[k])
+
+ def test_CreateForwardedZone(self):
+ payload = {
+ 'name': unique_zone_name(),
+ 'kind': 'Forwarded',
+ 'servers': ['8.8.8.8'],
+ 'recursion_desired': False
+ }
+ r = self.session.post(
+ self.url("/servers/localhost/zones"),
+ data=json.dumps(payload),
+ headers={'content-type': 'application/json'})
+ self.assertSuccessJson(r)
+ data = r.json()
+ # return values are normalized
+ payload['servers'][0] += ':53'
+ payload['name'] += '.'
+ for k in payload.keys():
+ self.assertEquals(data[k], payload[k])
+
+ def test_CreateForwardedRDZone(self):
+ payload = {
+ 'name': 'google.com',
+ 'kind': 'Forwarded',
+ 'servers': ['8.8.8.8'],
+ 'recursion_desired': True
+ }
+ r = self.session.post(
+ self.url("/servers/localhost/zones"),
+ data=json.dumps(payload),
+ headers={'content-type': 'application/json'})
+ self.assertSuccessJson(r)
+ data = r.json()
+ # return values are normalized
+ payload['servers'][0] += ':53'
+ payload['name'] += '.'
+ for k in payload.keys():
+ self.assertEquals(data[k], payload[k])
+
+ def test_CreateAuthZoneWithSymbols(self):
+ payload = {
+ 'name': 'foo/bar.'+unique_zone_name(),
+ 'kind': 'Native',
+ 'recursion_desired': False
+ }
+ r = self.session.post(
+ self.url("/servers/localhost/zones"),
+ data=json.dumps(payload),
+ headers={'content-type': 'application/json'})
+ self.assertSuccessJson(r)
+ data = r.json()
+ # return values are normalized
+ payload['name'] += '.'
+ expected_id = (payload['name'].replace('/', '=47'))
+ for k in payload.keys():
+ self.assertEquals(data[k], payload[k])
+ self.assertEquals(data['id'], expected_id)