static void storeChangedPTRs(UeberBackend& B, vector<DNSResourceRecord>& new_ptrs);
static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
+// QTypes that MUST NOT have multiple records of the same type in a given RRset.
+static const std::set<uint16_t> onlyOneEntryTypes = { QType::CNAME, QType::DNAME, QType::SOA };
+// QTypes that MUST NOT be used with any other QType on the same name.
+static const std::set<uint16_t> exclusiveEntryTypes = { QType::CNAME, QType::DNAME };
+
AuthWebServer::AuthWebServer() :
d_tid(0),
d_start(time(nullptr)),
}
static void gatherRecords(const Json container, const DNSName& qname, const QType qtype, const int ttl, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
- static const std::set<uint16_t> onlyOneEntryTypes = { QType::CNAME, QType::SOA };
UeberBackend B;
DNSResourceRecord rr;
rr.qname = qname;
validateGatheredRRType(rr);
const auto& items = container["records"].array_items();
- if (onlyOneEntryTypes.count(qtype.getCode()) != 0 && items.size() > 1) {
- throw ApiException("RRset for "+rr.qname.toString()+"/"+rr.qtype.getName()+" has more than one record");
- }
-
for(const auto& record : items) {
string content = stringFromJson(record, "content");
rr.disabled = boolFromJson(record, "disabled");
}
}
-/** Throws ApiException if records with duplicate name/type/content are present.
+/** Throws ApiException if records which violate RRset contraints are present.
* NOTE: sorts records in-place.
+ *
+ * Constraints being checked:
+ * *) no exact duplicates
+ * *) no duplicates for QTypes that can only be present once per RRset
+ * *) hostnames are hostnames
*/
-static void checkDuplicateRecords(vector<DNSResourceRecord>& records) {
+static void checkNewRecords(vector<DNSResourceRecord>& records) {
sort(records.begin(), records.end(),
[](const DNSResourceRecord& rec_a, const DNSResourceRecord& rec_b) -> bool {
/* we need _strict_ weak ordering */
return std::tie(rec_a.qname, rec_a.qtype, rec_a.content) < std::tie(rec_b.qname, rec_b.qtype, rec_b.content);
}
);
+
DNSResourceRecord previous;
for(const auto& rec : records) {
- if (previous.qtype == rec.qtype && previous.qname == rec.qname && previous.content == rec.content) {
- throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.getName() + " with content \"" + rec.content + "\"");
+ if (previous.qname == rec.qname) {
+ if (previous.qtype == rec.qtype) {
+ if (onlyOneEntryTypes.count(rec.qtype.getCode()) != 0) {
+ throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName()+" has more than one record");
+ }
+ if (previous.content == rec.content) {
+ throw ApiException("Duplicate record in RRset " + rec.qname.toString() + " IN " + rec.qtype.getName() + " with content \"" + rec.content + "\"");
+ }
+ } else if (exclusiveEntryTypes.count(rec.qtype.getCode()) != 0 || exclusiveEntryTypes.count(previous.qtype.getCode()) != 0) {
+ throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName()+": Conflicts with another RRset");
+ }
+ }
+
+ // Check if the DNSNames that should be hostnames, are hostnames
+ try {
+ checkHostnameCorrectness(rec);
+ } catch (const std::exception& e) {
+ throw ApiException("RRset "+rec.qname.toString()+" IN "+rec.qtype.getName() + " " + e.what());
}
+
previous = rec;
}
}
}
}
- checkDuplicateRecords(new_records);
+ checkNewRecords(new_records);
if (boolFromJson(document, "dnssec", false)) {
checkDefaultDNSSECAlgos();
if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
}
-
- // Check if the DNSNames that should be hostnames, are hostnames
- try {
- checkHostnameCorrectness(rr);
- } catch (const std::exception& e) {
- throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName() + " " + e.what());
- }
}
- checkDuplicateRecords(new_records);
+ checkNewRecords(new_records);
}
if (replace_comments) {
if (qtype.getCode() == 0) {
ent_present = true;
}
- if (qtype.getCode() == QType::CNAME && rr.qtype.getCode() != QType::CNAME) {
- throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Conflicts with pre-existing non-CNAME RRset");
- } else if (qtype.getCode() != QType::CNAME && rr.qtype.getCode() == QType::CNAME) {
- throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Conflicts with pre-existing CNAME RRset");
+ if (qtype.getCode() != rr.qtype.getCode()
+ && (exclusiveEntryTypes.count(qtype.getCode()) != 0
+ || exclusiveEntryTypes.count(rr.qtype.getCode()) != 0)) {
+ throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Conflicts with pre-existing RRset");
}
}
import time
import unittest
from copy import deepcopy
+from parameterized import parameterized
from pprint import pprint
from test_helper import ApiTestCase, unique_zone_name, is_auth, is_recursor, get_db_records, pdnsutil_rectify
self.assertEquals(data['name'], 'example.com.')
def test_import_zone_broken(self):
- payload = {}
+ payload = {
+ 'name': 'powerdns-broken.com',
+ 'kind': 'Master',
+ 'nameservers': [],
+ }
payload['zone'] = """
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
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("/api/v1/servers/localhost/zones"),
data=json.dumps(payload),
def test_import_zone_axfr_outofzone(self):
# Ensure we don't create out-of-zone records
- name = unique_zone_name()
- payload = {}
+ payload = {
+ 'name': unique_zone_name(),
+ 'kind': 'Master',
+ 'nameservers': [],
+ }
payload['zone'] = """
-NAME 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
-NAME 3600 IN NS powerdnssec2.ds9a.nl.
+%NAME% 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
+%NAME% 3600 IN NS powerdnssec2.ds9a.nl.
example.org. 3600 IN AAAA 2001:888:2000:1d::2
-NAME 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
-""".replace('NAME', name)
- payload['name'] = name
- payload['kind'] = 'Master'
- payload['nameservers'] = []
+%NAME% 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
+""".replace('%NAME%', payload['name'])
r = self.session.post(
self.url("/api/v1/servers/localhost/zones"),
data=json.dumps(payload),
self.assertEqual(r.json()['error'], 'RRset example.org. IN AAAA: Name is out of zone')
def test_import_zone_axfr(self):
- payload = {}
+ payload = {
+ 'name': 'powerdns.com.',
+ 'kind': 'Master',
+ 'nameservers': [],
+ 'soa_edit_api': '', # turn off so exact SOA comparison works.
+ }
payload['zone'] = """
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
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'] = []
- payload['soa_edit_api'] = '' # turn off so exact SOA comparison works.
r = self.session.post(
self.url("/api/v1/servers/localhost/zones"),
data=json.dumps(payload),
self.assertEqual(dbrec['content'], 'powerdnssec1.ds9a.nl')
def test_import_zone_bind(self):
- payload = {}
+ payload = {
+ 'name': 'example.org.',
+ 'kind': 'Master',
+ 'nameservers': [],
+ 'soa_edit_api': '', # turn off so exact SOA comparison works.
+ }
payload['zone'] = """
$TTL 86400 ; 24 hours could have been written as 24h or 1d
; $TTL used for all RRs without explicit TTL value
bill IN A 192.168.0.3
fred IN A 192.168.0.4
"""
- payload['name'] = 'example.org.'
- payload['kind'] = 'Master'
- payload['nameservers'] = []
- payload['soa_edit_api'] = '' # turn off so exact SOA comparison works.
r = self.session.post(
self.url("/api/v1/servers/localhost/zones"),
data=json.dumps(payload),
eq_zone_rrsets(data['rrsets'], expected)
+ def test_import_zone_bind_cname_apex(self):
+ payload = {
+ 'name': unique_zone_name(),
+ 'kind': 'Master',
+ 'nameservers': [],
+ }
+ payload['zone'] = """
+$ORIGIN %NAME%
+@ IN SOA ns1.example.org. hostmaster.example.org. (2002022401 3H 15 1W 3H)
+@ IN NS ns1.example.org.
+@ IN NS ns2.smokeyjoe.com.
+@ IN CNAME www.example.org.
+""".replace('%NAME%', payload['name'])
+ r = self.session.post(
+ self.url("/api/v1/servers/localhost/zones"),
+ data=json.dumps(payload),
+ headers={'content-type': 'application/json'})
+ self.assertEquals(r.status_code, 422)
+ self.assertIn('Conflicts with another RRset', r.json()['error'])
+
def test_export_zone_json(self):
name, payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
# export it
self.assertEquals(r.status_code, 422)
self.assertIn('unknown type', r.json()['error'])
- def test_rrset_cname_and_other(self):
+ @parameterized.expand([
+ ('CNAME', ),
+ ('DNAME', ),
+ ])
+ def test_rrset_exclusive_and_other(self, qtype):
name, payload, zone = self.create_zone()
rrset = {
'changetype': 'replace',
'name': name,
- 'type': 'CNAME',
+ 'type': qtype,
'ttl': 3600,
'records': [
{
r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
headers={'content-type': 'application/json'})
self.assertEquals(r.status_code, 422)
- self.assertIn('Conflicts with pre-existing non-CNAME RRset', r.json()['error'])
+ self.assertIn('Conflicts with pre-existing RRset', r.json()['error'])
- def test_rrset_other_and_cname(self):
+ @parameterized.expand([
+ ('CNAME', ),
+ ('DNAME', ),
+ ])
+ def test_rrset_other_and_exclusive(self, qtype):
name, payload, zone = self.create_zone()
rrset = {
'changetype': 'replace',
'name': 'sub.'+name,
- 'type': 'CNAME',
+ 'type': qtype,
'ttl': 3600,
'records': [
{
r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
headers={'content-type': 'application/json'})
self.assertEquals(r.status_code, 422)
- self.assertIn('Conflicts with pre-existing CNAME RRset', r.json()['error'])
-
- def test_rrset_multiple_cnames(self):
+ self.assertIn('Conflicts with pre-existing RRset', r.json()['error'])
+
+ @parameterized.expand([
+ ('SOA', ['ns1.example.org. test@example.org. 10 10800 3600 604800 3600', 'ns2.example.org. test@example.org. 10 10800 3600 604800 3600']),
+ ('CNAME', ['01.example.org.', '02.example.org.']),
+ ('DNAME', ['01.example.org.', '02.example.org.']),
+ ])
+ def test_rrset_single_qtypes(self, qtype, contents):
name, payload, zone = self.create_zone()
rrset = {
'changetype': 'replace',
'name': 'sub.'+name,
- 'type': 'CNAME',
+ 'type': qtype,
'ttl': 3600,
'records': [
{
- "content": "01.example.org.",
+ "content": contents[0],
"disabled": False
},
{
- "content": "02.example.org.",
+ "content": contents[1],
"disabled": False
}
]
r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
headers={'content-type': 'application/json'})
self.assertEquals(r.status_code, 422)
- self.assertIn('/CNAME has more than one record', r.json()['error'])
+ self.assertIn('IN ' + qtype + ' has more than one record', r.json()['error'])
def test_create_zone_with_leading_space(self):
# Actual regression.