d_queries.get5()<<", "<<
d_queries.get10()<<". Max queries/second: "<<d_queries.getMax()<<
"<br>"<<endl;
-
+
if(d_cachemisses.get10()+d_cachehits.get10()>0)
ret<<"Cache hitrate, 1, 5, 10 minute averages: "<<
makePercentage((d_cachehits.get1()*100.0)/((d_cachehits.get1())+(d_cachemisses.get1())))<<", "<<
resp->status = 200;
}
+/** Helper to build a record content as needed. */
+static inline string makeRecordContent(const QType& qtype, const string& content, bool noDot) {
+ // noDot: for backend storage, pass true. for API users, pass false.
+ return DNSRecordContent::mastermake(qtype.getCode(), 1, content)->getZoneRepresentation(noDot);
+}
+
+/** "Normalize" record content for API consumers. */
+static inline string makeApiRecordContent(const QType& qtype, const string& content) {
+ return makeRecordContent(qtype, content, false);
+}
+
+/** "Normalize" record content for backend storage. */
+static inline string makeBackendRecordContent(const QType& qtype, const string& content) {
+ return makeRecordContent(qtype, content, true);
+}
+
static void fillZoneInfo(const DomainInfo& di, Value& jdi, Document& doc) {
DNSSECKeeper dk;
jdi.SetObject();
object.AddMember("type", jtype, doc.GetAllocator());
object.AddMember("ttl", rr.ttl, doc.GetAllocator());
object.AddMember("disabled", rr.disabled, doc.GetAllocator());
- Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
+ Value jcontent(makeApiRecordContent(rr.qtype, rr.content).c_str(), doc.GetAllocator()); // copy
object.AddMember("content", jcontent, doc.GetAllocator());
records.PushBack(object, doc.GetAllocator());
}
if (records.IsArray()) {
for (SizeType idx = 0; idx < records.Size(); ++idx) {
const Value& record = records[idx];
- rr.qname = DNSName(stringFromJson(record, "name"));
+ rr.qname = dnsnameFromJson(record, "name");
rr.qtype = stringFromJson(record, "type");
- rr.content = stringFromJson(record, "content");
+ string content = stringFromJson(record, "content");
rr.auth = 1;
rr.ttl = intFromJson(record, "ttl");
rr.disabled = boolFromJson(record, "disabled");
throw ApiException("Record "+rr.qname.toString()+"/"+stringFromJson(record, "type")+" is of unknown type");
}
+ // validate that the client sent something we can actually parse, and require that data to be dotted.
try {
- shared_ptr<DNSRecordContent> drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, rr.content));
- string tmp = drc->serialize(rr.qname);
+ //shared_ptr<DNSRecordContent> drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, content));
+ //string tmp = drc->serialize(rr.qname);
if (rr.qtype.getCode() != QType::AAAA) {
- tmp = drc->getZoneRepresentation();
- if (!pdns_iequals(tmp, rr.content)) {
+ string tmp = makeApiRecordContent(rr.qtype, content);
+ if (!pdns_iequals(tmp, content)) {
throw std::runtime_error("Not in expected format (parsed as '"+tmp+"')");
}
} else {
struct in6_addr tmpbuf;
- if (inet_pton(AF_INET6, rr.content.c_str(), &tmpbuf) != 1 || rr.content.find('.') != string::npos) {
+ if (inet_pton(AF_INET6, content.c_str(), &tmpbuf) != 1 || content.find('.') != string::npos) {
throw std::runtime_error("Invalid IPv6 address");
}
}
+ rr.content = makeBackendRecordContent(rr.qtype, content);
}
catch(std::exception& e)
{
- throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" '"+rr.content+"': "+e.what());
+ throw ApiException("Record "+rr.qname.toString()+"/"+rr.qtype.getName()+" '"+content+"': "+e.what());
}
if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
if(rr.qtype.getCode() == QType::SOA)
seenSOA=true;
- // rr.qname = stripDot(rr.qname);
new_records.push_back(rr);
}
}
DomainInfo di;
Document document;
req->json(document);
- string zonename = stringFromJson(document, "name");
- DNSName dzonename(zonename);
-
- // strip trailing dot (from spec PoV this is wrong, but be nice to clients)
- if (zonename.size() > 0 && zonename.substr(zonename.size()-1) == ".") {
- zonename.resize(zonename.size()-1);
- }
+ DNSName zonename = dnsnameFromJson(document, "name");
+ apiCheckNameAllowedCharacters(zonename.toString());
string zonestring = stringFromJson(document, "zone", "");
- bool exists = B.getDomainInfo(dzonename, di);
+ bool exists = B.getDomainInfo(zonename, di);
if(exists)
- throw ApiException("Domain '"+zonename+"' already exists");
+ throw ApiException("Domain '"+zonename.toString()+"' already exists");
// validate 'kind' is set
DomainInfo::DomainKind zonekind = DomainInfo::stringToKind(stringFromJson(document, "kind"));
if (records.IsArray()) {
gatherRecords(document, new_records, new_ptrs);
} else if (zonestring != "") {
- gatherRecordsFromZone(document, new_records, DNSName(zonename));
+ gatherRecordsFromZone(document, new_records, zonename);
}
gatherComments(document, new_comments, false);
for(auto& rr : new_records) {
- if (!rr.qname.isPartOf(dzonename) && rr.qname != dzonename)
+ if (!rr.qname.isPartOf(zonename) && rr.qname != zonename)
throw ApiException("RRset "+rr.qname.toString()+" IN "+rr.qtype.getName()+": Name is out of zone");
+ apiCheckNameAllowedCharacters(rr.qname.toString());
- if (rr.qtype.getCode() == QType::SOA && rr.qname==dzonename) {
+ if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
have_soa = true;
increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
+ // fixup dots after serializeSOAData/increaseSOARecord
+ rr.content = makeBackendRecordContent(rr.qtype, rr.content);
}
}
// synthesize RRs as needed
DNSResourceRecord autorr;
- autorr.qname = dzonename;
+ autorr.qname = zonename;
autorr.auth = 1;
autorr.ttl = ::arg().asNum("default-ttl");
if (!have_soa && zonekind != DomainInfo::Slave) {
// synthesize a SOA record so the zone "really" exists
-
+ string soa = (boost::format("%s %s %lu")
+ % ::arg()["default-soa-name"]
+ % (::arg().isEmpty("default-soa-mail") ? (DNSName("hostmaster.") + zonename).toString() : ::arg()["default-soa-mail"])
+ % intFromJson(document, "serial", 0)
+ ).str();
SOAData sd;
- sd.qname = dzonename;
- sd.nameserver = DNSName(arg()["default-soa-name"]);
- if (!arg().isEmpty("default-soa-mail")) {
- sd.hostmaster = DNSName(arg()["default-soa-mail"]); // needs attodot?
- // attodot(sd.hostmaster); FIXME400
- } else {
- sd.hostmaster = DNSName("hostmaster.") + dzonename;
- }
- sd.serial = intFromJson(document, "serial", 0);
- sd.ttl = autorr.ttl;
- sd.refresh = ::arg().asNum("soa-refresh-default");
- sd.retry = ::arg().asNum("soa-retry-default");
- sd.expire = ::arg().asNum("soa-expire-default");
- sd.default_ttl = ::arg().asNum("soa-minimum-ttl");
-
- autorr.content = serializeSOAData(sd);
+ fillSOAData(soa, sd); // fills out default values for us
autorr.qtype = "SOA";
+ autorr.content = serializeSOAData(sd);
increaseSOARecord(autorr, soa_edit_api_kind, soa_edit_kind);
+ // fixup dots after serializeSOAData/increaseSOARecord
+ autorr.content = makeBackendRecordContent(autorr.qtype, autorr.content);
new_records.push_back(autorr);
}
for (SizeType i = 0; i < nameservers.Size(); ++i) {
if (!nameservers[i].IsString())
throw ApiException("Nameservers must be strings");
- autorr.content = nameservers[i].GetString();
+ string nameserver = nameservers[i].GetString();
+ if (!isCanonical(nameserver))
+ throw ApiException("Nameserver is not canonical: '" + nameserver + "'");
+ try {
+ // ensure the name parses
+ autorr.content = DNSName(nameserver).toStringNoDot();
+ } catch (...) {
+ throw ApiException("Unable to parse DNS Name for NS '" + nameserver + "'");
+ }
autorr.qtype = "NS";
new_records.push_back(autorr);
}
}
// no going back after this
- if(!B.createDomain(dzonename))
- throw ApiException("Creating domain '"+zonename+"' failed");
+ if(!B.createDomain(zonename))
+ throw ApiException("Creating domain '"+zonename.toString()+"' failed");
- if(!B.getDomainInfo(dzonename, di))
- throw ApiException("Creating domain '"+zonename+"' failed: lookup of domain ID failed");
+ if(!B.getDomainInfo(zonename, di))
+ throw ApiException("Creating domain '"+zonename.toString()+"' failed: lookup of domain ID failed");
- di.backend->startTransaction(dzonename, di.id);
+ di.backend->startTransaction(zonename, di.id);
for(auto rr : new_records) {
rr.domain_id = di.id;
di.backend->feedRecord(rr);
}
- for(Comment& c : new_comments) {
+ for(Comment& c : new_comments) {
c.domain_id = di.id;
di.backend->feedComment(c);
}
- updateDomainSettingsFromDocument(di, dzonename, document);
+ updateDomainSettingsFromDocument(di, zonename, document);
di.backend->commitTransaction();
- fillZone(dzonename, resp);
+ fillZone(zonename, resp);
resp->status = 201;
return;
}
throw HttpMethodNotAllowedException();
}
-// static string makeDotted(string in) {
-// if (in.empty()) {
-// return ".";
-// }
-// if (in[in.size()-1] != '.') {
-// return in + ".";
-// }
-// return in;
-// }
-
static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
DNSName zonename = apiZoneIdToName(req->parameters["id"]);
if (!rr.qtype.getCode())
continue; // skip empty non-terminals
- string content = rr.content;
-
- switch(rr.qtype.getCode()) {
- case QType::SOA:
- fillSOAData(rr.content, sd);
- /* sd.nameserver = sd.nameserver.toString();
- sd.hostmaster = sd.hostmaster.toString(); */ // XXX DNSName pain - these looked like noops?
- content = serializeSOAData(sd);
- break;
- case QType::MX:
- case QType::SRV:
- case QType::CNAME:
- case QType::NS:
- case QType::AFSDB:
- content = rr.content;
- break;
- default:
- break;
- }
-
ss <<
rr.qname.toString() << "\t" <<
rr.ttl << "\t" <<
rr.qtype.getName() << "\t" <<
- content <<
+ makeApiRecordContent(rr.qtype, rr.content) <<
endl;
}
if (!IpToU32(rr.content, &ip)) {
throw ApiException("PTR: Invalid IP address given");
}
- ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa")
+ ptr->qname = DNSName((boost::format("%u.%u.%u.%u.in-addr.arpa.")
% ((ip >> 24) & 0xff)
% ((ip >> 16) & 0xff)
% ((ip >> 8) & 0xff)
% ((ip ) & 0xff)
- ).str());
+ ).str());
} else if (rr.qtype.getCode() == QType::AAAA) {
ComboAddress ca(rr.content);
char buf[3];
string tmp = ss.str();
tmp.resize(tmp.size()-1); // remove last dot
// reverse and append arpa domain
- ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa");
+ ptr->qname = DNSName(string(tmp.rbegin(), tmp.rend())) + DNSName("ip6.arpa.");
} else {
throw ApiException("Unsupported PTR source '" + rr.qname.toString() + "' type '" + rr.qtype.getName() + "'");
}
const Value& rrset = rrsets[rrsetIdx];
string changetype;
QType qtype;
- DNSName qname(stringFromJson(rrset, "name"));
+ DNSName qname = dnsnameFromJson(rrset, "name");
+ apiCheckNameAllowedCharacters(qname.toString());
qtype = stringFromJson(rrset, "type");
changetype = toUpper(stringFromJson(rrset, "changetype"));
}
}
else if (changetype == "REPLACE") {
- // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
+ // we only validate for REPLACE, as DELETE can be used to "fix" out of zone records.
if (!qname.isPartOf(zonename) && qname != zonename)
throw ApiException("RRset "+qname.toString()+" IN "+qtype.getName()+": Name is out of zone");
- new_records.clear();
+ new_records.clear();
new_comments.clear();
// new_ptrs is merged
gatherRecords(rrset, new_records, new_ptrs);
if (rr.qtype.getCode() == QType::SOA && rr.qname==zonename) {
soa_edit_done = increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
+ rr.content = makeBackendRecordContent(rr.qtype, rr.content);
}
}
rr.auth = 1;
rr.ttl = sd.ttl;
increaseSOARecord(rr, soa_edit_api_kind, soa_edit_kind);
+ // fixup dots after serializeSOAData/increaseSOARecord
+ rr.content = makeBackendRecordContent(rr.qtype, rr.content);
if (!di.backend->replaceRRSet(di.id, rr.qname, rr.qtype, vector<DNSResourceRecord>(1, rr))) {
throw ApiException("Hosting backend does not support editing records.");
vector<DomainInfo> domains;
vector<DNSResourceRecord> result_rr;
vector<Comment> result_c;
- map<int,DNSName> zoneIdZone;
- map<int,DNSName>::iterator val;
+ map<int,DomainInfo> zoneIdZone;
+ map<int,DomainInfo>::iterator val;
Document doc;
doc.SetArray();
B.getAllDomains(&domains, true);
for(const DomainInfo di: domains)
- {
+ {
if (ents < maxEnts && sm.match(di.zone)) {
Value object;
object.SetObject();
+ Value jzoneId(apiZoneNameToId(di.zone).c_str(), doc.GetAllocator()); // copy
object.AddMember("object_type", "zone", doc.GetAllocator());
- object.AddMember("zone_id", di.id, doc.GetAllocator());
+ object.AddMember("zone_id", jzoneId, doc.GetAllocator());
Value jzoneName(di.zone.toString().c_str(), doc.GetAllocator()); // copy
object.AddMember("name", jzoneName, doc.GetAllocator());
doc.PushBack(object, doc.GetAllocator());
ents++;
}
- zoneIdZone[di.id] = di.zone; // populate cache
+ zoneIdZone[di.id] = di; // populate cache
}
if (B.searchRecords(q, maxEnts, result_rr))
Value object;
object.SetObject();
object.AddMember("object_type", "record", doc.GetAllocator());
- object.AddMember("zone_id", rr.domain_id, doc.GetAllocator());
if ((val = zoneIdZone.find(rr.domain_id)) != zoneIdZone.end()) {
- Value zname(val->second.toString().c_str(), doc.GetAllocator()); // copy
+ Value jzoneId(apiZoneNameToId(val->second.zone).c_str(), doc.GetAllocator()); // copy
+ object.AddMember("zone_id", jzoneId, doc.GetAllocator());
+ Value zname(val->second.zone.toString().c_str(), doc.GetAllocator()); // copy
object.AddMember("zone", zname, doc.GetAllocator()); // copy
}
Value jname(rr.qname.toString().c_str(), doc.GetAllocator()); // copy
object.AddMember("type", jtype, doc.GetAllocator());
object.AddMember("ttl", rr.ttl, doc.GetAllocator());
object.AddMember("disabled", rr.disabled, doc.GetAllocator());
- Value jcontent(rr.content.c_str(), doc.GetAllocator()); // copy
+ Value jcontent(makeApiRecordContent(rr.qtype, rr.content).c_str(), doc.GetAllocator()); // copy
object.AddMember("content", jcontent, doc.GetAllocator());
doc.PushBack(object, doc.GetAllocator());
}
{
for(const Comment &c: result_c)
{
-
Value object;
object.SetObject();
object.AddMember("object_type", "comment", doc.GetAllocator());
- object.AddMember("zone_id", c.domain_id, doc.GetAllocator());
if ((val = zoneIdZone.find(c.domain_id)) != zoneIdZone.end()) {
- Value zname(val->second.toString().c_str(), doc.GetAllocator()); // copy
+ Value jzoneId(apiZoneNameToId(val->second.zone).c_str(), doc.GetAllocator()); // copy
+ object.AddMember("zone_id", jzoneId, doc.GetAllocator());
+ Value zname(val->second.zone.toString().c_str(), doc.GetAllocator()); // copy
object.AddMember("zone", zname, doc.GetAllocator()); // copy
}
Value jname(c.qname.c_str(), doc.GetAllocator()); // copy
import json
import time
import unittest
-from test_helper import ApiTestCase, unique_zone_name, is_auth, is_recursor
+from test_helper import ApiTestCase, unique_zone_name, is_auth, is_recursor, eq_zone_dict, get_db_records
class Zones(ApiTestCase):
payload = {
'name': name,
'kind': 'Native',
- 'nameservers': ['ns1.example.com', 'ns2.example.com']
+ 'nameservers': ['ns1.example.com.', 'ns2.example.com.']
}
for k, v in kwargs.items():
if v is None:
del payload[k]
else:
payload[k] = v
- print payload
+ print "sending", payload
r = self.session.post(
self.url("/api/v1/servers/localhost/zones"),
data=json.dumps(payload),
headers={'content-type': 'application/json'})
self.assert_success_json(r)
self.assertEquals(r.status_code, 201)
- return payload, r.json()
+ reply = r.json()
+ print "reply", reply
+ return payload, reply
@unittest.skipIf(not is_auth(), "Not applicable")
self.assertEquals(data[k], payload[k])
self.assertEquals(data['comments'], [])
# validate generated SOA
+ expected_soa = "a.misconfigured.powerdns.server. hostmaster." + payload['name'] + " " + \
+ str(payload['serial']) + " 10800 3600 604800 3600"
self.assertEquals(
[r['content'] for r in data['records'] if r['type'] == 'SOA'][0],
- "a.misconfigured.powerdns.server hostmaster." + payload['name'] + " " + str(payload['serial']) +
- " 10800 3600 604800 3600"
+ expected_soa
)
+ # Because we had confusion about dots, check that the DB is without dots.
+ dbrecs = get_db_records(payload['name'], 'SOA')
+ self.assertEqual(dbrecs[0]['content'], expected_soa.replace('. ', ' '))
def test_create_zone_with_soa_edit_api(self):
# soa_edit_api wins over serial
# check our comment has appeared
self.assertEquals(data['comments'], comments)
+ def test_create_zone_uncanonical_nameservers(self):
+ name = unique_zone_name()
+ payload = {
+ 'name': name,
+ 'kind': 'Native',
+ 'nameservers': ['uncanon.example.com']
+ }
+ print payload
+ 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('Nameserver is not canonical', r.json()['error'])
+
+ def test_create_auth_zone_no_name(self):
+ name = unique_zone_name()
+ payload = {
+ 'name': '',
+ 'kind': 'Native',
+ }
+ print payload
+ 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('is not canonical', r.json()['error'])
+
def test_create_zone_with_custom_soa(self):
name = unique_zone_name()
records = [
{
- "name": name,
- 'type': 'soa', # test uppercasing of type, too.
- "ttl": 3600,
- "content": "ns1.example.net testmaster@example.net 10 10800 3600 604800 3600",
- "disabled": False
+ u"name": name,
+ u"type": u"soa", # test uppercasing of type, too.
+ u"ttl": 3600,
+ u"content": u"ns1.example.net. testmaster@example.net. 10 10800 3600 604800 3600",
+ u"disabled": False
}
]
- payload, data = self.create_zone(name=name, records=records)
+ payload, data = self.create_zone(name=name, records=records, soa_edit_api='')
records[0]['type'] = records[0]['type'].upper()
self.assertEquals([r for r in data['records'] if r['type'] == records[0]['type']], records)
+ dbrecs = get_db_records(name, records[0]['type'])
+ self.assertEqual(dbrecs[0]['content'], records[0]['content'].replace('. ', ' '))
+
+ def test_create_zone_double_dot(self):
+ name = 'test..' + unique_zone_name()
+ payload = {
+ 'name': name,
+ 'kind': 'Native',
+ 'nameservers': ['ns1.example.com.']
+ }
+ print payload
+ 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('Unable to parse DNS Name', r.json()['error'])
- def test_create_zone_trailing_dot(self):
- # Trailing dots should not end up in the zone name.
- basename = unique_zone_name()
- payload, data = self.create_zone(name=basename+'.')
- self.assertEquals(data['name'], basename)
+ def test_create_zone_restricted_chars(self):
+ name = 'test:' + unique_zone_name() # : isn't good as a name.
+ payload = {
+ 'name': name,
+ 'kind': 'Native',
+ 'nameservers': ['ns1.example.com']
+ }
+ print payload
+ 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('contains unsupported characters', r.json()['error'])
def test_create_zone_with_symbols(self):
payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
name = payload['name']
- expected_id = (name.replace('/', '=2F')) + '.'
+ expected_id = name.replace('/', '=2F')
for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
self.assertIn(k, data)
if k in payload:
self.assertEquals(data[k], payload[k])
self.assertEquals(data['id'], expected_id)
+ dbrecs = get_db_records(name, 'SOA')
+ self.assertEqual(dbrecs[0]['name'], name.rstrip('.'))
def test_create_zone_with_nameservers_non_string(self):
# ensure we don't crash
def test_get_zone_with_symbols(self):
payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
name = payload['name']
- zone_id = (name.replace('/', '=2F')) + '.'
+ zone_id = (name.replace('/', '=2F'))
r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id))
data = r.json()
for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'dnssec'):
def test_get_zone(self):
r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
domains = r.json()
- example_com = [domain for domain in domains if domain['name'] == u'example.com'][0]
+ example_com = [domain for domain in domains if domain['name'] == u'example.com.'][0]
r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + example_com['id']))
self.assert_success_json(r)
data = r.json()
for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
self.assertIn(k, data)
- self.assertEquals(data['name'], 'example.com')
+ self.assertEquals(data['name'], 'example.com.')
def test_import_zone_broken(self):
payload = {}
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['name'] = 'powerdns-broken.com.'
payload['kind'] = 'Master'
payload['nameservers'] = []
r = self.session.post(
headers={'content-type': 'application/json'})
self.assertEquals(r.status_code, 422)
+ def test_import_zone_axfr_outofzone(self):
+ # Ensure we don't create out-of-zone records
+ name = unique_zone_name()
+ payload = {}
+ payload['zone'] = """
+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'] = []
+ 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.assertEqual(r.json()['error'], 'RRset example.org. IN AAAA: Name is out of zone')
+
def test_import_zone_axfr(self):
payload = {}
payload['zone'] = """
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['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),
'MX': [
{ 'content': '0 xs.powerdns.com.' } ],
'A': [
- { 'content': '82.94.213.34', 'name': 'powerdns.com' } ],
+ { 'content': '82.94.213.34', 'name': 'powerdns.com.' } ],
'AAAA': [
- { 'content': '2001:888:2000:1d::2', 'name': 'powerdns.com' } ]
+ { 'content': '2001:888:2000:1d::2', 'name': 'powerdns.com.' } ]
}
- counter = {}
- for et in expected.keys():
- counter[et] = len(expected[et])
- for ev in expected[et]:
- for ret in data['records']:
- if 'name' in ev:
- if ret['name'] == ev['name'] and ret['content'] == ev['content'].rstrip('.'):
- counter[et] = counter[et]-1
- continue
- if ret['content'] == ev['content'].rstrip('.'):
- counter[et] = counter[et]-1
- self.assertEquals(counter[et], 0)
+ eq_zone_dict(data['records'], expected)
+
+ # noDot check
+ dbrecs = get_db_records(payload['name'], 'NS')
+ self.assertEqual(dbrecs[0]['content'], 'powerdnssec2.ds9a.nl')
def test_import_zone_bind(self):
payload = {}
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
+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
+fred IN A 192.168.0.4
"""
- payload['name'] = 'example.org'
+ 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),
'MX': [
{ 'content': '10 mail.another.com.' } ],
'A': [
- { 'content': '192.168.0.1', 'name': 'ns1.example.org' },
- { 'content': '192.168.0.2', 'name': 'www.example.org' },
- { 'content': '192.168.0.3', 'name': 'bill.example.org' },
- { 'content': '192.168.0.4', 'name': 'fred.example.org' } ],
+ { 'content': '192.168.0.1', 'name': 'ns1.example.org.' },
+ { 'content': '192.168.0.2', 'name': 'www.example.org.' },
+ { 'content': '192.168.0.3', 'name': 'bill.example.org.' },
+ { 'content': '192.168.0.4', 'name': 'fred.example.org.' } ],
'CNAME': [
- { 'content': 'www.example.org', 'name': 'ftp.example.org' } ]
+ { 'content': 'www.example.org.', 'name': 'ftp.example.org.' } ]
}
- counter = {}
- for et in expected.keys():
- counter[et] = len(expected[et])
- for ev in expected[et]:
- for ret in data['records']:
- if 'name' in ev:
- if ret['name'] == ev['name'] and ret['content'] == ev['content'].rstrip('.'):
- counter[et] = counter[et]-1
- continue
- if ret['content'] == ev['content'].rstrip('.'):
- counter[et] = counter[et]-1
- self.assertEquals(counter[et], 0)
+ eq_zone_dict(data['records'], expected)
def test_export_zone_json(self):
- payload, zone = self.create_zone(nameservers=['ns1.foo.com', 'ns2.foo.com'], soa_edit_api='')
+ payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
name = payload['name']
# export it
r = self.session.get(
self.assert_success_json(r)
data = r.json()
self.assertIn('zone', data)
- expected_data = [name + '.\t3600\tNS\tns1.foo.com.',
- name + '.\t3600\tNS\tns2.foo.com.',
- name + '.\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
- '. 0 10800 3600 604800 3600']
+ expected_data = [name + '\t3600\tNS\tns1.foo.com.',
+ name + '\t3600\tNS\tns2.foo.com.',
+ name + '\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
+ ' 0 10800 3600 604800 3600']
self.assertEquals(data['zone'].strip().split('\n'), expected_data)
def test_export_zone_text(self):
- payload, zone = self.create_zone(nameservers=['ns1.foo.com', 'ns2.foo.com'], soa_edit_api='')
+ payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
name = payload['name']
# export it
r = self.session.get(
headers={'accept': '*/*'}
)
data = r.text.strip().split("\n")
- expected_data = [name + '.\t3600\tNS\tns1.foo.com.',
- name + '.\t3600\tNS\tns2.foo.com.',
- name + '.\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
- '. 0 10800 3600 604800 3600']
+ expected_data = [name + '\t3600\tNS\tns1.foo.com.',
+ name + '\t3600\tNS\tns2.foo.com.',
+ name + '\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
+ ' 0 10800 3600 604800 3600']
self.assertEquals(data, expected_data)
def test_update_zone(self):
"name": name,
"type": "NS",
"ttl": 3600,
- "content": "ns1.bar.com",
+ "content": "ns1.bar.com.",
"disabled": False
},
{
"name": name,
"type": "NS",
"ttl": 1800,
- "content": "ns2-disabled.bar.com",
+ "content": "ns2-disabled.bar.com.",
"disabled": True
}
]
r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
rrset['type'] = rrset['type'].upper()
data = r.json()['records']
- recs = [rec for rec in data if rec['type'] == rrset['type'] and rec['name'] == rrset['name']]
+ recs = [rec for rec in data if rec['type'].upper() == rrset['type'].upper() and rec['name'].upper() == rrset['name'].upper()]
self.assertEquals(recs, rrset['records'])
def test_zone_rr_update_mx(self):
"name": name,
"type": "MX",
"ttl": 3600,
- "content": "10 mail.example.org",
+ "content": "10 mail.example.org.",
"disabled": False
}
]
"name": name,
"type": "NS",
"ttl": 3600,
- "content": "ns9999.example.com",
+ "content": "ns9999.example.com.",
"disabled": False
}
]
"name": name,
"type": "MX",
"ttl": 3600,
- "content": "10 mx444.example.com",
+ "content": "10 mx444.example.com.",
"disabled": False
}
]
"name": name,
"type": "SOA",
"ttl": 3600,
- "content": "ns1.bar.com hostmaster.foo.org 1 1 1 1 1",
+ "content": "ns1.bar.com. hostmaster.foo.org. 1 1 1 1 1",
"disabled": True
}
]
"name": name,
"type": "NS",
"ttl": 3600,
- "content": "ns1.bar.com",
+ "content": "ns1.bar.com.",
"disabled": False
}
]
"name": 'blah.'+name,
"type": "NS",
"ttl": 3600,
- "content": "ns1.bar.com",
+ "content": "ns1.bar.com.",
"disabled": False
}
]
# replace with qname mismatch
rrset = {
'changetype': 'replace',
- 'name': 'not-in-zone',
+ 'name': 'not-in-zone.',
'type': 'NS',
'records': [
{
"name": name,
"type": "NS",
"ttl": 3600,
- "content": "ns1.bar.com",
+ "content": "ns1.bar.com.",
"disabled": False
}
]
self.assertEquals(r.status_code, 422)
self.assertIn('out of zone', r.json()['error'])
+ def test_zone_rr_update_restricted_chars(self):
+ payload, zone = self.create_zone()
+ name = payload['name']
+ # replace with qname mismatch
+ rrset = {
+ 'changetype': 'replace',
+ 'name': 'test:' + name,
+ 'type': 'NS',
+ 'records': [
+ {
+ "name": 'test:' + name,
+ "type": "NS",
+ "ttl": 3600,
+ "content": "ns1.bar.com.",
+ "disabled": False
+ }
+ ]
+ }
+ payload = {'rrsets': [rrset]}
+ 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('contains unsupported characters', r.json()['error'])
+
def test_rrset_unknown_type(self):
payload, zone = self.create_zone()
name = payload['name']
name = payload['name']
rrset = {
'changetype': 'delete',
- 'name': 'not-in-zone',
+ 'name': 'not-in-zone.',
'type': 'NS'
}
payload = {'rrsets': [rrset]}
"name": name,
"type": "NS",
"ttl": 3600,
- "content": "ns1.bar.com",
+ "content": "ns1.bar.com.",
"disabled": False
}
]
self.assertEquals(data['comments'], rrset['comments'])
def test_zone_auto_ptr_ipv4(self):
- revzone = '0.2.192.in-addr.arpa'
+ revzone = '0.2.192.in-addr.arpa.'
self.create_zone(name=revzone)
payload, zone = self.create_zone()
name = payload['name']
u'disabled': False,
u'ttl': 3600,
u'type': u'PTR',
- u'name': u'2.0.2.192.in-addr.arpa'
+ u'name': u'2.0.2.192.in-addr.arpa.'
}])
def test_zone_auto_ptr_ipv6(self):
# 2001:DB8::bb:aa
- revzone = '8.b.d.0.1.0.0.2.ip6.arpa'
+ revzone = '8.b.d.0.1.0.0.2.ip6.arpa.'
self.create_zone(name=revzone)
payload, zone = self.create_zone()
name = payload['name']
u'disabled': False,
u'ttl': 3600,
u'type': u'PTR',
- u'name': u'a.a.0.0.b.b.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa'
+ u'name': u'a.a.0.0.b.b.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.'
}])
def test_search_rr_exact_zone(self):
name = unique_zone_name()
- self.create_zone(name=name)
- r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name))
+ self.create_zone(name=name, serial=22, soa_edit_api='')
+ r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name.rstrip('.')))
self.assert_success_json(r)
print r.json()
- self.assertEquals(r.json(), [{u'object_type': u'zone', u'name': name, u'zone_id': name+'.'}])
+ self.assertEquals(r.json(), [
+ {u'object_type': u'zone', u'name': name, u'zone_id': name},
+ {u'content': u'a.misconfigured.powerdns.server. hostmaster.'+name+' 22 10800 3600 604800 3600',
+ u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
+ u'ttl': 3600, u'type': u'SOA', u'name': name},
+ {u'content': u'ns1.example.com.',
+ u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
+ u'ttl': 3600, u'type': u'NS', u'name': name},
+ {u'content': u'ns2.example.com.',
+ u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
+ u'ttl': 3600, u'type': u'NS', u'name': name},
+ ])
def test_search_rr_substring(self):
- name = 'search-rr-zone.name'
+ name = 'search-rr-zone.name.'
self.create_zone(name=name)
r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*rr-zone*"))
self.assert_success_json(r)
self.assertEquals(len(r.json()), 4)
def test_search_rr_case_insensitive(self):
- name = 'search-rr-insenszone.name'
+ name = 'search-rr-insenszone.name.'
self.create_zone(name=name)
r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*rr-insensZONE*"))
self.assert_success_json(r)
self.session.delete(self.url("/api/v1/servers/localhost/zones/=2E"))
def test_create_zone(self):
- payload, data = self.create_zone(name='', serial=22, soa_edit_api='')
+ payload, data = self.create_zone(name='.', serial=22, soa_edit_api='')
for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api', 'soa_edit', 'account'):
self.assertIn(k, data)
if k in payload:
# validate generated SOA
self.assertEquals(
[r['content'] for r in data['records'] if r['type'] == 'SOA'][0],
- "a.misconfigured.powerdns.server hostmaster." + payload['name'] + " " + str(payload['serial']) +
+ "a.misconfigured.powerdns.server. hostmaster. " + str(payload['serial']) +
" 10800 3600 604800 3600"
)
# Regression test: verify zone list works
for k in ('name', 'kind'):
self.assertIn(k, data)
self.assertEquals(data[k], payload[k])
- self.assertEqual(data['records'][0]['name'], '')
+ self.assertEqual(data['records'][0]['name'], '.')
def test_update_zone(self):
- payload, zone = self.create_zone(name='')
- name = ''
+ payload, zone = self.create_zone(name='.')
zone_id = '=2E'
# update, set as Master and enable SOA-EDIT-API
payload = {
def test_create_auth_zone(self):
payload, data = self.create_zone(kind='Native')
- # return values are normalized
- payload['name'] += '.'
for k in payload.keys():
self.assertEquals(data[k], payload[k])
+ def test_create_zone_no_name(self):
+ name = unique_zone_name()
+ payload = {
+ 'name': '',
+ 'kind': 'Native',
+ 'servers': ['8.8.8.8'],
+ 'recursion_desired': False,
+ }
+ print payload
+ 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('is not canonical', r.json()['error'])
+
def test_create_forwarded_zone(self):
payload, data = self.create_zone(kind='Forwarded', rd=False, servers=['8.8.8.8'])
# return values are normalized
payload['servers'][0] += ':53'
- payload['name'] += '.'
for k in payload.keys():
self.assertEquals(data[k], payload[k])
def test_create_forwarded_rd_zone(self):
- payload, data = self.create_zone(name='google.com', kind='Forwarded', rd=True, servers=['8.8.8.8'])
+ payload, data = self.create_zone(name='google.com.', kind='Forwarded', rd=True, servers=['8.8.8.8'])
# return values are normalized
payload['servers'][0] += ':53'
- payload['name'] += '.'
for k in payload.keys():
self.assertEquals(data[k], payload[k])
def test_create_auth_zone_with_symbols(self):
payload, data = self.create_zone(name='foo/bar.'+unique_zone_name(), kind='Native')
- # return values are normalized
- payload['name'] += '.'
expected_id = (payload['name'].replace('/', '=2F'))
for k in payload.keys():
self.assertEquals(data[k], payload[k])
def test_rename_auth_zone(self):
payload, data = self.create_zone(kind='Native')
- name = payload['name'] + '.'
+ name = payload['name']
# now rename it
payload = {
'name': 'renamed-'+name,
self.assertNotIn('Content-Type', r.headers)
def test_search_rr_exact_zone(self):
- name = unique_zone_name() + '.'
+ name = unique_zone_name()
self.create_zone(name=name, kind='Native')
r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name))
self.assert_success_json(r)
self.assertEquals(r.json(), [{u'type': u'zone', u'name': name, u'zone_id': name}])
def test_search_rr_substring(self):
- name = 'search-rr-zone.name'
+ name = 'search-rr-zone.name.'
self.create_zone(name=name, kind='Native')
r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=rr-zone"))
self.assert_success_json(r)