]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Implement a zone import through the API 1532/head
authorMark Schouten <mark@tuxis.nl>
Fri, 13 Jun 2014 08:53:29 +0000 (10:53 +0200)
committerMark Schouten <mark@tuxis.nl>
Mon, 21 Jul 2014 10:18:17 +0000 (12:18 +0200)
pdns/ws-auth.cc
pdns/zoneparser-tng.cc
pdns/zoneparser-tng.hh
regression-tests.api/test_Zones.py

index 07359046c3b776bcea6d9736e83a14b6d75e4ed3..8b8226efcd174d8c3f8a9503ec765a915327f8a4 100644 (file)
@@ -41,6 +41,7 @@
 #include "version.hh"
 #include "dnsseckeeper.hh"
 #include <iomanip>
+#include "zoneparser-tng.hh"
 
 #ifdef HAVE_CONFIG_H
 # include <config.h>
@@ -550,6 +551,33 @@ static void apiZoneCryptokeys(HttpRequest* req, HttpResponse* resp) {
   resp->setBody(doc);
 }
 
+static void gatherRecordsFromZone(const Value &container, vector<DNSResourceRecord>& new_records, string zonename) {
+  DNSResourceRecord rr;
+  vector<string> zonedata;
+  stringtok(zonedata, stringFromJson(container, "zone"), "\r\n");
+
+  ZoneParserTNG zpt(zonedata, zonename);
+
+  bool seenSOA=false;
+
+  string comment = "Imported via the API";
+
+  try {
+    while(zpt.get(rr, &comment)) {
+      if(seenSOA && rr.qtype.getCode() == QType::SOA)
+        continue;
+      if(rr.qtype.getCode() == QType::SOA)
+        seenSOA=true;
+
+      rr.qname = stripDot(rr.qname);
+      new_records.push_back(rr);
+    }
+  }
+  catch(std::exception& ae) {
+    throw ApiException("An error occured while parsing the zonedata: "+string(ae.what()));
+  }
+}
+
 static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
   UeberBackend B;
   DNSSECKeeper dk;
@@ -559,6 +587,8 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
     req->json(document);
     string zonename = stringFromJson(document, "name");
     string dotsuffix = "." + zonename;
+    string zonestring = stringFromJson(document, "zone", "");
+
     // TODO: better validation of zonename
     if(zonename.empty())
       throw ApiException("Zone name empty");
@@ -575,6 +605,10 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
     // validate 'kind' is set
     DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
 
+    const Value &records = document["records"];
+    if (records.IsArray() && zonestring != "")
+      throw ApiException("You cannot give zonedata AND records");
+
     const Value &nameservers = document["nameservers"];
     if (!nameservers.IsArray() && zonekind != DomainInfo::Slave)
       throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
@@ -588,7 +622,13 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
     vector<DNSResourceRecord> new_records;
     vector<Comment> new_comments;
     vector<DNSResourceRecord> new_ptrs;
-    gatherRecords(document, new_records, new_ptrs);
+
+    if (records.IsArray()) {
+      gatherRecords(document, new_records, new_ptrs);
+    } else if (zonestring != "") {
+      gatherRecordsFromZone(document, new_records, zonename);
+    }
+
     gatherComments(document, new_comments, false);
 
     DNSResourceRecord rr;
index c5c03ecff1b079f91ca27e27f8a2d3799f6d7132..4dd41b69e60c883baceaa7acb865f3b7c846ea6b 100644 (file)
@@ -41,6 +41,16 @@ ZoneParserTNG::ZoneParserTNG(const string& fname, const string& zname, const str
   stackFile(fname);
 }
 
+ZoneParserTNG::ZoneParserTNG(const vector<string> zonedata, const string& zname):
+                                                                        d_zonename(zname), d_defaultttl(3600), 
+                                                                        d_havedollarttl(false)
+{
+  d_zonename = toCanonic("", d_zonename);
+  d_zonedata = zonedata;
+  d_zonedataline = d_zonedata.begin();
+  d_fromfile = false;
+}
+
 void ZoneParserTNG::stackFile(const std::string& fname)
 {
   FILE *fp=fopen(fname.c_str(), "r");
@@ -49,6 +59,7 @@ void ZoneParserTNG::stackFile(const std::string& fname)
 
   filestate fs(fp, fname);
   d_filestates.push(fs);
+  d_fromfile = true;
 }
 
 ZoneParserTNG::~ZoneParserTNG()
@@ -226,6 +237,9 @@ bool findAndElide(string& line, char c)
 
 string ZoneParserTNG::getLineOfFile()
 {
+  if (d_zonedata.size() > 0)
+    return "on line "+lexical_cast<string>(std::distance(d_zonedata.begin(), d_zonedataline))+" of given string";
+
   return "on line "+lexical_cast<string>(d_filestates.top().d_lineno)+" of file '"+d_filestates.top().d_filename+"'";
 }
 
@@ -256,7 +270,7 @@ bool ZoneParserTNG::get(DNSResourceRecord& rr, std::string* comment)
       d_defaultttl=makeTTLFromZone(trim_right_copy_if(makeString(d_line, parts[1]), is_any_of(";")));
       d_havedollarttl=true;
     }
-    else if(pdns_iequals(command,"$INCLUDE") && parts.size() > 1) {
+    else if(pdns_iequals(command,"$INCLUDE") && parts.size() > 1 && d_fromfile) {
       string fname=unquotify(makeString(d_line, parts[1]));
       if(!fname.empty() && fname[0]!='/' && !d_reldir.empty())
         fname=d_reldir+"/"+fname;
@@ -442,6 +456,14 @@ bool ZoneParserTNG::get(DNSResourceRecord& rr, std::string* comment)
 
 bool ZoneParserTNG::getLine()
 {
+  if (d_zonedata.size() > 0) {
+    if (d_zonedataline != d_zonedata.end()) {
+      d_line = *d_zonedataline;
+      d_zonedataline++;
+      return true;
+    }
+    return false;
+  }
   while(!d_filestates.empty()) {
     if(stringfgets(d_filestates.top().d_fp, d_line)) {
       d_filestates.top().d_lineno++;
index 54c73c1b88c869cffff797d97458a2e4f12070c0..79e58a8d4b7477883810cf59922d9cda56474a07 100644 (file)
@@ -33,6 +33,7 @@ class ZoneParserTNG
 {
 public:
   ZoneParserTNG(const string& fname, const string& zname="", const string& reldir="");
+  ZoneParserTNG(const vector<string> zonedata, const string& zname);
 
   ~ZoneParserTNG();
   bool get(DNSResourceRecord& rr, std::string* comment=0);
@@ -48,8 +49,11 @@ private:
   string d_line;
   string d_prevqname;
   string d_zonename;
+  vector<string> d_zonedata;
+  vector<string>::iterator d_zonedataline;
   int d_defaultttl;
   bool d_havedollarttl;
+  bool d_fromfile;
   uint32_t d_templatecounter, d_templatestop, d_templatestep;
   string d_templateline;
   parts_t d_templateparts;
index 113f41af3903e95e9f4589f1bcd5ec3000f24159..8ab8fa4c896d634d8a76d95dd730942d1d8e0fc9 100644 (file)
@@ -202,6 +202,153 @@ class AuthZones(ApiTestCase):
             self.assertIn(k, data)
         self.assertEquals(data['name'], 'example.com')
 
+    def test_import_zone_broken(self):
+        payload = {}
+        payload['zone'] = """
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
+flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
+;; WARNING: recursion requested but not available
+
+;; OPT PSEUDOSECTION:
+; EDNS: version: 0, flags:; udp: 1680
+;; QUESTION SECTION:
+;powerdns.com.                  IN      SOA
+
+;; ANSWER SECTION:
+powerdns-broken.com.           86400   IN      SOA     powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
+powerdns-broken.com.           3600    IN      NS      powerdnssec2.ds9a.nl.
+powerdns-broken.com.           3600    IN      AAAA    2001:888:2000:1d::2
+powerdns-broken.com.           86400   IN      A       82.94.213.34
+powerdns-broken.com.           3600    IN      MX      0 xs.powerdns.com.
+powerdns-broken.com.           3600    IN      NS      powerdnssec1.ds9a.nl.
+powerdns-broken.com.           86400   IN      SOA     powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
+"""
+        payload['name'] = 'powerdns-broken.com'
+        payload['kind'] = 'Master'
+        payload['nameservers'] = []
+        r = self.session.post(
+            self.url("/servers/localhost/zones"),
+            data=json.dumps(payload),
+            headers={'content-type': 'application/json'})
+        self.assertEquals(r.status_code, 422)
+
+    def test_import_zone_axfr(self):
+        payload = {}
+        payload['zone'] = """
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
+;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
+;; WARNING: recursion requested but not available
+
+;; OPT PSEUDOSECTION:
+; EDNS: version: 0, flags:; udp: 1680
+;; QUESTION SECTION:
+;powerdns.com.                  IN      SOA
+
+;; ANSWER SECTION:
+powerdns.com.           86400   IN      SOA     powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
+powerdns.com.           3600    IN      NS      powerdnssec2.ds9a.nl.
+powerdns.com.           3600    IN      AAAA    2001:888:2000:1d::2
+powerdns.com.           86400   IN      A       82.94.213.34
+powerdns.com.           3600    IN      MX      0 xs.powerdns.com.
+powerdns.com.           3600    IN      NS      powerdnssec1.ds9a.nl.
+powerdns.com.           86400   IN      SOA     powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
+"""
+        payload['name'] = 'powerdns.com'
+        payload['kind'] = 'Master'
+        payload['nameservers'] = []
+        r = self.session.post(
+            self.url("/servers/localhost/zones"),
+            data=json.dumps(payload),
+            headers={'content-type': 'application/json'})
+        self.assert_success_json(r)
+        data = r.json()
+        self.assertIn('name', data)
+        self.assertIn('records', data)
+
+        expected = {}
+        expected['NS'] = []
+        expected['NS'].append('powerdnssec1.ds9a.nl.')
+        expected['NS'].append('powerdnssec2.ds9a.nl.')
+        expected['SOA'] = []
+        expected['SOA'].append('powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800')
+        expected['MX'] = []
+        expected['MX'].append('0 xs.powerdns.com.')
+        expected['A'] = []
+        expected['A'].append('82.94.213.34')
+        expected['AAAA'] = []
+        expected['AAAA'].append('2001:888:2000:1d::2')
+
+        counter = {}
+        for et in expected.keys():
+            counter[et] = len(expected[et])
+            for ev in expected[et]:
+                for ret in data['records']:
+                    if ret['content'] == ev.rstrip('.'):
+                        counter[et] = counter[et]-1
+            self.assertEquals(counter[et], 0)
+
+    def test_import_zone_bind(self):
+        payload = {}
+        payload['zone'] = """
+$TTL    86400 ; 24 hours could have been written as 24h or 1d
+; $TTL used for all RRs without explicit TTL value
+$ORIGIN example.org.
+@  1D  IN  SOA ns1.example.org. hostmaster.example.org. (
+                  2002022401 ; serial
+                  3H ; refresh
+                  15 ; retry
+                  1w ; expire
+                  3h ; minimum
+                 )
+       IN  NS     ns1.example.org. ; in the domain
+       IN  NS     ns2.smokeyjoe.com. ; external to domain
+       IN MX  10 mail.another.com. ; external mail provider
+; server host definitions
+ns1    IN A      192.168.0.1  ;name server definition     
+www    IN  A      192.168.0.2  ;web server definition
+ftp    IN CNAME  www.example.org.  ;ftp server definition
+; non server domain hosts
+bill   IN  A      192.168.0.3
+fred   IN  A      192.168.0.4 
+"""
+        payload['name'] = 'example.org'
+        payload['kind'] = 'Master'
+        payload['nameservers'] = []
+        r = self.session.post(
+            self.url("/servers/localhost/zones"),
+            data=json.dumps(payload),
+            headers={'content-type': 'application/json'})
+        self.assert_success_json(r)
+        data = r.json()
+        self.assertIn('name', data)
+        self.assertIn('records', data)
+
+        expected = {}
+        expected['NS'] = []
+        expected['NS'].append('ns1.example.org.')
+        expected['NS'].append('ns2.smokeyjoe.com.')
+        expected['SOA'] = []
+        expected['SOA'].append('ns1.example.org. hostmaster.example.org. 2002022401 10800 15 604800 10800')
+        expected['MX'] = []
+        expected['MX'].append('10 mail.another.com.')
+        expected['A'] = []
+        expected['A'].append('192.168.0.1')
+        expected['A'].append('192.168.0.2')
+        expected['A'].append('192.168.0.3')
+        expected['A'].append('192.168.0.4')
+        expected['CNAME'] = []
+        expected['CNAME'].append('www.example.org.')
+
+        counter = {}
+        for et in expected.keys():
+            counter[et] = len(expected[et])
+            found = False
+            for ev in expected[et]:
+                for ret in data['records']:
+                    if ret['content'] == ev.rstrip('.'):
+                        counter[et] = counter[et]-1
+            self.assertEquals(counter[et], 0)
+
     def test_export_zone_json(self):
         payload, zone = self.create_zone(nameservers=['ns1.foo.com', 'ns2.foo.com'])
         name = payload['name']