From 0f0e73fe6e2a03c9cf244076f62664c708b7407f Mon Sep 17 00:00:00 2001 From: Mark Schouten Date: Fri, 13 Jun 2014 10:53:29 +0200 Subject: [PATCH] Implement a zone import through the API --- pdns/ws-auth.cc | 42 ++++++++- pdns/zoneparser-tng.cc | 24 ++++- pdns/zoneparser-tng.hh | 4 + regression-tests.api/test_Zones.py | 147 +++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 2 deletions(-) diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 07359046c..8b8226efc 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -41,6 +41,7 @@ #include "version.hh" #include "dnsseckeeper.hh" #include +#include "zoneparser-tng.hh" #ifdef HAVE_CONFIG_H # include @@ -550,6 +551,33 @@ static void apiZoneCryptokeys(HttpRequest* req, HttpResponse* resp) { resp->setBody(doc); } +static void gatherRecordsFromZone(const Value &container, vector& new_records, string zonename) { + DNSResourceRecord rr; + vector 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 new_records; vector new_comments; vector 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; diff --git a/pdns/zoneparser-tng.cc b/pdns/zoneparser-tng.cc index c5c03ecff..4dd41b69e 100644 --- a/pdns/zoneparser-tng.cc +++ b/pdns/zoneparser-tng.cc @@ -41,6 +41,16 @@ ZoneParserTNG::ZoneParserTNG(const string& fname, const string& zname, const str stackFile(fname); } +ZoneParserTNG::ZoneParserTNG(const vector 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(std::distance(d_zonedata.begin(), d_zonedataline))+" of given string"; + return "on line "+lexical_cast(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++; diff --git a/pdns/zoneparser-tng.hh b/pdns/zoneparser-tng.hh index 54c73c1b8..79e58a8d4 100644 --- a/pdns/zoneparser-tng.hh +++ b/pdns/zoneparser-tng.hh @@ -33,6 +33,7 @@ class ZoneParserTNG { public: ZoneParserTNG(const string& fname, const string& zname="", const string& reldir=""); + ZoneParserTNG(const vector 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 d_zonedata; + vector::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; diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index 113f41af3..8ab8fa4c8 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -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'] -- 2.40.0