extern StatBag S;
+static void patchZone(HttpRequest* req, HttpResponse* resp);
+static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr);
+
AuthWebServer::AuthWebServer()
{
d_start=time(0);
out["uptime"] = lexical_cast<string>(time(0) - s_starttime);
}
-static void patchZone(HttpRequest* req, HttpResponse* resp);
+static void gatherRecords(const Value& container, vector<DNSResourceRecord>& new_records, vector<DNSResourceRecord>& new_ptrs) {
+ UeberBackend B;
+ DNSResourceRecord rr;
+ const Value& records = container["records"];
+ if (records.IsArray()) {
+ for (SizeType idx = 0; idx < records.Size(); ++idx) {
+ const Value& record = records[idx];
+ rr.qname = stringFromJson(record, "name");
+ rr.qtype = stringFromJson(record, "type");
+ rr.content = stringFromJson(record, "content");
+ rr.auth = 1;
+ rr.ttl = intFromJson(record, "ttl");
+ rr.priority = intFromJson(record, "priority");
+ rr.disabled = boolFromJson(record, "disabled");
+
+ string temp_content = rr.content;
+ if (rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV)
+ temp_content = lexical_cast<string>(rr.priority)+" "+rr.content;
+
+ try {
+ shared_ptr<DNSRecordContent> drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, temp_content));
+ string tmp = drc->serialize(rr.qname);
+ }
+ catch(std::exception& e)
+ {
+ throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": "+e.what());
+ }
+
+ if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
+ boolFromJson(record, "set-ptr", false) == true) {
+ DNSResourceRecord ptr;
+ makePtr(rr, &ptr);
+
+ // verify that there's a zone for the PTR
+ DNSPacket fakePacket;
+ SOAData sd;
+ fakePacket.qtype = QType::PTR;
+ if (!B.getAuth(&fakePacket, &sd, ptr.qname, 0))
+ throw ApiException("Could not find domain for PTR '"+ptr.qname+"' requested for '"+ptr.content+"'");
+
+ ptr.domain_id = sd.domain_id;
+ new_ptrs.push_back(ptr);
+ }
+
+ new_records.push_back(rr);
+ }
+ }
+}
+
+static void gatherComments(const Value& container, vector<Comment>& new_comments, bool use_name_type_from_container) {
+ Comment c;
+ if (use_name_type_from_container) {
+ c.qname = stringFromJson(container, "name");
+ c.qtype = stringFromJson(container, "type");
+ }
+
+ time_t now = time(0);
+ const Value& comments = container["comments"];
+ if (comments.IsArray()) {
+ for(SizeType idx = 0; idx < comments.Size(); ++idx) {
+ const Value& comment = comments[idx];
+ if (!use_name_type_from_container) {
+ c.qname = stringFromJson(comment, "name");
+ c.qtype = stringFromJson(comment, "type");
+ }
+ c.modified_at = intFromJson(comment, "modified_at", now);
+ c.content = stringFromJson(comment, "content");
+ c.account = stringFromJson(comment, "account");
+ new_comments.push_back(c);
+ }
+ }
+}
static void updateDomainSettingsFromDocument(const DomainInfo& di, const string& zonename, Document& document) {
string master;
Document document;
req->json(document);
string zonename = stringFromJson(document, "name");
+ string dotsuffix = "." + zonename;
// TODO: better validation of zonename
if(zonename.empty())
throw ApiException("Zone name empty");
stringFromJson(document, "kind");
const Value &nameservers = document["nameservers"];
- if (!nameservers.IsArray() || nameservers.Size() == 0)
- throw ApiException("Need at least one nameserver");
+ if (!nameservers.IsArray())
+ throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)");
- for (SizeType i = 0; i < nameservers.Size(); ++i) {
- if (!nameservers[i].IsString()) {
- throw ApiException("Nameservers must be strings.");
- }
- }
+ string soa_edit_api_kind;
+ if (document["soa_edit_api"].IsString())
+ soa_edit_api_kind = document["soa_edit_api"].GetString();
- // no going back after this
- if(!B.createDomain(zonename))
- throw ApiException("Creating domain '"+zonename+"' failed");
+ // if records/comments are given, load and check them
+ bool have_soa = false;
+ vector<DNSResourceRecord> new_records;
+ vector<Comment> new_comments;
+ vector<DNSResourceRecord> new_ptrs;
+ gatherRecords(document, new_records, new_ptrs);
+ gatherComments(document, new_comments, false);
- if(!B.getDomainInfo(zonename, di))
- throw ApiException("Creating domain '"+zonename+"' failed: lookup of domain ID failed");
+ DNSResourceRecord rr;
- vector<DNSResourceRecord> rrset;
+ BOOST_FOREACH(rr, new_records) {
+ if (!iends_with(rr.qname, dotsuffix) && !pdns_iequals(rr.qname, zonename))
+ throw ApiException("RRset "+rr.qname+" IN "+rr.qtype.getName()+": Name is out of zone");
+
+ if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) {
+ have_soa = true;
+ editSOARecord(rr, soa_edit_api_kind);
+ }
+ }
- // create SOA record so zone "really" exists
- DNSResourceRecord rr;
rr.qname = zonename;
- rr.content = (boost::format("%s hostmaster@%s %d")
- % nameservers[SizeType(0)].GetString()
- % zonename
- % intFromJson(document, "serial", 1)
- ).str();
- SOAData sd;
- fillSOAData(rr.content, sd);
- rr.content = serializeSOAData(sd);
- rr.qtype = "SOA";
- rr.domain_id = di.id;
rr.auth = 1;
- rr.ttl = ::arg().asNum( "default-ttl" );
+ rr.ttl = ::arg().asNum("default-ttl");
rr.priority = 0;
- rrset.push_back(rr);
+ if (!have_soa) {
+ // synthesize a SOA record so the zone "really" exists
+
+ SOAData sd;
+ sd.qname = zonename;
+ sd.nameserver = arg()["default-soa-name"];
+ if (!arg().isEmpty("default-soa-mail")) {
+ sd.hostmaster = arg()["default-soa-mail"];
+ attodot(sd.hostmaster);
+ } else {
+ sd.hostmaster = "hostmaster." + zonename;
+ }
+ sd.serial = intFromJson(document, "serial", 0);
+ sd.ttl = rr.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");
+
+ rr.content = serializeSOAData(sd);
+ rr.qtype = "SOA";
+ editSOARecord(rr, soa_edit_api_kind);
+ new_records.push_back(rr);
+ }
+
+ // create NS records if nameservers are given
for (SizeType i = 0; i < nameservers.Size(); ++i) {
+ if (!nameservers[i].IsString())
+ throw ApiException("Nameservers must be strings");
rr.content = nameservers[i].GetString();
rr.qtype = "NS";
- rrset.push_back(rr);
+ new_records.push_back(rr);
}
+ // no going back after this
+ if(!B.createDomain(zonename))
+ throw ApiException("Creating domain '"+zonename+"' failed");
+
+ if(!B.getDomainInfo(zonename, di))
+ throw ApiException("Creating domain '"+zonename+"' failed: lookup of domain ID failed");
+
di.backend->startTransaction(zonename, di.id);
- BOOST_FOREACH(rr, rrset) {
+
+ BOOST_FOREACH(rr, new_records) {
+ rr.domain_id = di.id;
di.backend->feedRecord(rr);
}
- di.backend->commitTransaction();
+ BOOST_FOREACH(Comment& c, new_comments) {
+ c.domain_id = di.id;
+ di.backend->feedComment(c);
+ }
updateDomainSettingsFromDocument(di, zonename, document);
+ di.backend->commitTransaction();
+
fillZone(zonename, resp);
return;
}
throw ApiException("Could not find domain '"+zonename+"'");
string dotsuffix = "." + zonename;
+ vector<DNSResourceRecord> new_records;
+ vector<Comment> new_comments;
vector<DNSResourceRecord> new_ptrs;
Document document;
}
}
else if (changetype == "REPLACE") {
- vector<DNSResourceRecord> new_records;
- vector<Comment> new_comments;
- bool replace_records = false;
- bool replace_comments = false;
-
- // gather records
- DNSResourceRecord rr;
- const Value& records = rrset["records"];
- if (records.IsArray()) {
- replace_records = true;
- for (SizeType idx = 0; idx < records.Size(); ++idx) {
- const Value& record = records[idx];
- rr.qname = stringFromJson(record, "name");
- rr.content = stringFromJson(record, "content");
- rr.qtype = stringFromJson(record, "type");
- rr.domain_id = di.id;
- rr.auth = 1;
- rr.ttl = intFromJson(record, "ttl");
- rr.priority = intFromJson(record, "priority");
- rr.disabled = boolFromJson(record, "disabled");
-
- if (rr.qname != qname || rr.qtype != qtype)
- throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": Record wrongly bundled with RRset " + qname + "/" + qtype.getName());
-
- string temp_content = rr.content;
- if (rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV)
- temp_content = lexical_cast<string>(rr.priority)+" "+rr.content;
-
- try {
- shared_ptr<DNSRecordContent> drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, temp_content));
- string tmp = drc->serialize(rr.qname);
- }
- catch(std::exception& e)
- {
- throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": "+e.what());
- }
-
- if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) &&
- boolFromJson(record, "set-ptr", false) == true) {
- DNSResourceRecord ptr;
- makePtr(rr, &ptr);
-
- // verify that there's a zone for the PTR
- DNSPacket fakePacket;
- SOAData sd;
- fakePacket.qtype = QType::PTR;
- if (!B.getAuth(&fakePacket, &sd, ptr.qname, 0))
- throw ApiException("Could not find domain for PTR '"+ptr.qname+"' requested for '"+ptr.content+"'");
-
- ptr.domain_id = sd.domain_id;
- new_ptrs.push_back(ptr);
- }
-
- if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) {
- soa_edit_done = editSOARecord(rr, soa_edit_api_kind);
- }
-
- new_records.push_back(rr);
+ new_records.clear();
+ new_comments.clear();
+ // new_ptrs is merged
+ gatherRecords(rrset, new_records, new_ptrs);
+ gatherComments(rrset, new_comments, true);
+
+ BOOST_FOREACH(DNSResourceRecord& rr, new_records) {
+ rr.domain_id = di.id;
+
+ if (rr.qname != qname || rr.qtype != qtype)
+ throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": Record wrongly bundled with RRset " + qname + "/" + qtype.getName());
+
+ if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) {
+ soa_edit_done = editSOARecord(rr, soa_edit_api_kind);
}
}
- // gather comments
- Comment c;
- c.domain_id = di.id;
- c.qname = qname;
- c.qtype = qtype;
- time_t now = time(0);
- const Value& comments = rrset["comments"];
- if (comments.IsArray()) {
- replace_comments = true;
- for(SizeType idx = 0; idx < comments.Size(); ++idx) {
- const Value& comment = comments[idx];
- c.modified_at = intFromJson(comment, "modified_at", now);
- c.content = stringFromJson(comment, "content");
- c.account = stringFromJson(comment, "account");
- new_comments.push_back(c);
- }
+ BOOST_FOREACH(Comment& c, new_comments) {
+ c.domain_id = di.id;
}
+ bool replace_records = rrset["records"].IsArray();
+ bool replace_comments = rrset["comments"].IsArray();
+
if (!replace_records && !replace_comments) {
throw ApiException("No change for RRset " + qname + "/" + qtype.getName());
}
return (payload, r.json())
def test_CreateZone(self):
- payload, data = self.create_zone()
+ payload, data = self.create_zone(serial=22)
for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api'):
self.assertIn(k, data)
if k in payload:
self.assertEquals(data[k], payload[k])
self.assertEquals(data['comments'], [])
+ # 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'])+" 10800 3600 604800 3600"
+ )
def test_CreateZoneWithSoaEditApi(self):
- payload, data = self.create_zone(soa_edit_api='EPOCH')
- for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api'):
+ # soa_edit_api wins over serial
+ payload, data = self.create_zone(soa_edit_api='EPOCH', serial=10)
+ for k in ('soa_edit_api', ):
self.assertIn(k, data)
if k in payload:
self.assertEquals(data[k], payload[k])
- self.assertEquals(data['comments'], [])
+ # generated EPOCH serial surely is > fixed serial we passed in
+ print data
+ self.assertGreater(data['serial'], payload['serial'])
+ soa_serial = int([r['content'].split(' ')[2] for r in data['records'] if r['type'] == 'SOA'][0])
+ self.assertGreater(soa_serial, payload['serial'])
+ self.assertEquals(soa_serial, data['serial'])
+
+ def test_CreateZoneWithRecords(self):
+ name = unique_zone_name()
+ records = [
+ {
+ "name": name,
+ "type": "A",
+ "priority": 0,
+ "ttl": 3600,
+ "content": "4.3.2.1",
+ "disabled": False
+ }
+ ]
+ payload, data = self.create_zone(name=name, records=records)
+ # check our record has appeared
+ self.assertEquals([r for r in data['records'] if r['type'] == records[0]['type']], records)
+
+ def test_CreateZoneWithComments(self):
+ name = unique_zone_name()
+ comments = [
+ {
+ 'name': name,
+ 'type': 'SOA',
+ 'account': 'test1',
+ 'content': 'blah blah',
+ 'modified_at': 11112,
+ }
+ ]
+ payload, data = self.create_zone(name=name, comments=comments)
+ # check our comment has appeared
+ self.assertEquals(data['comments'], comments)
+
+ def test_CreateZoneWithCustomSOA(self):
+ name = unique_zone_name()
+ records = [
+ {
+ "name": name,
+ "type": "SOA",
+ "priority": 0,
+ "ttl": 3600,
+ "content": "ns1.example.net testmaster@example.net 10 10800 3600 604800 3600",
+ "disabled": False
+ }
+ ]
+ payload, data = self.create_zone(name=name, records=records)
+ self.assertEquals([r for r in data['records'] if r['type'] == records[0]['type']], records)
def test_CreateZoneTrailingDot(self):
# Trailing dots should not end up in the zone name.