};
}
-static void fillZone(const DNSName& zonename, HttpResponse* resp) {
+static bool shouldDoRRSets(HttpRequest* req) {
+ if (req->getvars.count("rrsets") == 0 || req->getvars["rrsets"] == "true")
+ return true;
+ if (req->getvars["rrsets"] == "false")
+ return false;
+ throw ApiException("'rrsets' request parameter value '"+req->getvars["rrsets"]+"' is not supported");
+}
+
+static void fillZone(const DNSName& zonename, HttpResponse* resp, bool doRRSets) {
UeberBackend B;
DomainInfo di;
if(!B.getDomainInfo(zonename, di))
string soa_edit;
di.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa_edit);
doc["soa_edit"] = soa_edit;
-
- vector<DNSResourceRecord> records;
- vector<Comment> comments;
-
- // load all records + sort
- {
- DNSResourceRecord rr;
- di.backend->list(zonename, di.id, true); // incl. disabled
- while(di.backend->get(rr)) {
- if (!rr.qtype.getCode())
- continue; // skip empty non-terminals
- records.push_back(rr);
+ string nsec3param;
+ di.backend->getDomainMetadataOne(zonename, "NSEC3PARAM", nsec3param);
+ doc["nsec3param"] = nsec3param;
+ string nsec3narrow;
+ bool nsec3narrowbool = false;
+ di.backend->getDomainMetadataOne(zonename, "NSEC3NARROW", nsec3narrow);
+ if (nsec3narrow == "1")
+ nsec3narrowbool = true;
+ doc["nsec3narrow"] = nsec3narrowbool;
+
+ string api_rectify;
+ di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
+ doc["api_rectify"] = (api_rectify == "1");
+
+ if (doRRSets) {
+ vector<DNSResourceRecord> records;
+ vector<Comment> comments;
+
+ // load all records + sort
+ {
+ DNSResourceRecord rr;
+ di.backend->list(zonename, di.id, true); // incl. disabled
+ while(di.backend->get(rr)) {
+ if (!rr.qtype.getCode())
+ continue; // skip empty non-terminals
+ records.push_back(rr);
+ }
+ sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
+ if (a.qname == b.qname) {
+ return b.qtype < a.qtype;
+ }
+ return b.qname < a.qname;
+ });
}
- sort(records.begin(), records.end(), [](const DNSResourceRecord& a, const DNSResourceRecord& b) {
- if (a.qname == b.qname) {
- return b.qtype < a.qtype;
- }
- return b.qname < a.qname;
- });
- }
- // load all comments + sort
- {
- Comment comment;
- di.backend->listComments(di.id);
- while(di.backend->getComment(comment)) {
- comments.push_back(comment);
+ // load all comments + sort
+ {
+ Comment comment;
+ di.backend->listComments(di.id);
+ while(di.backend->getComment(comment)) {
+ comments.push_back(comment);
+ }
+ sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
+ if (a.qname == b.qname) {
+ return b.qtype < a.qtype;
+ }
+ return b.qname < a.qname;
+ });
}
- sort(comments.begin(), comments.end(), [](const Comment& a, const Comment& b) {
- if (a.qname == b.qname) {
- return b.qtype < a.qtype;
- }
- return b.qname < a.qname;
- });
- }
- Json::array rrsets;
- Json::object rrset;
- Json::array rrset_records;
- Json::array rrset_comments;
- DNSName current_qname;
- QType current_qtype;
- uint32_t ttl;
- auto rit = records.begin();
- auto cit = comments.begin();
-
- while (rit != records.end() || cit != comments.end()) {
- if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() <= rit->qname.toString() || cit->qtype < rit->qtype || cit->qtype == rit->qtype))) {
- current_qname = rit->qname;
- current_qtype = rit->qtype;
- ttl = rit->ttl;
- } else {
- current_qname = cit->qname;
- current_qtype = cit->qtype;
- ttl = 0;
- }
+ Json::array rrsets;
+ Json::object rrset;
+ Json::array rrset_records;
+ Json::array rrset_comments;
+ DNSName current_qname;
+ QType current_qtype;
+ uint32_t ttl;
+ auto rit = records.begin();
+ auto cit = comments.begin();
+
+ while (rit != records.end() || cit != comments.end()) {
+ if (cit == comments.end() || (rit != records.end() && (cit->qname.toString() <= rit->qname.toString() || cit->qtype < rit->qtype || cit->qtype == rit->qtype))) {
+ current_qname = rit->qname;
+ current_qtype = rit->qtype;
+ ttl = rit->ttl;
+ } else {
+ current_qname = cit->qname;
+ current_qtype = cit->qtype;
+ ttl = 0;
+ }
- while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
- ttl = min(ttl, rit->ttl);
- rrset_records.push_back(Json::object {
- { "disabled", rit->disabled },
- { "content", makeApiRecordContent(rit->qtype, rit->content) }
- });
- rit++;
- }
- while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
- rrset_comments.push_back(Json::object {
- { "modified_at", (double)cit->modified_at },
- { "account", cit->account },
- { "content", cit->content }
- });
- cit++;
+ while(rit != records.end() && rit->qname == current_qname && rit->qtype == current_qtype) {
+ ttl = min(ttl, rit->ttl);
+ rrset_records.push_back(Json::object {
+ { "disabled", rit->disabled },
+ { "content", makeApiRecordContent(rit->qtype, rit->content) }
+ });
+ rit++;
+ }
+ while (cit != comments.end() && cit->qname == current_qname && cit->qtype == current_qtype) {
+ rrset_comments.push_back(Json::object {
+ { "modified_at", (double)cit->modified_at },
+ { "account", cit->account },
+ { "content", cit->content }
+ });
+ cit++;
+ }
+
+ rrset["name"] = current_qname.toString();
+ rrset["type"] = current_qtype.getName();
+ rrset["records"] = rrset_records;
+ rrset["comments"] = rrset_comments;
+ rrset["ttl"] = (double)ttl;
+ rrsets.push_back(rrset);
+ rrset.clear();
+ rrset_records.clear();
+ rrset_comments.clear();
}
- rrset["name"] = current_qname.toString();
- rrset["type"] = current_qtype.getName();
- rrset["records"] = rrset_records;
- rrset["comments"] = rrset_comments;
- rrset["ttl"] = (double)ttl;
- rrsets.push_back(rrset);
- rrset.clear();
- rrset_records.clear();
- rrset_comments.clear();
+ doc["rrsets"] = rrsets;
}
- doc["rrsets"] = rrsets;
-
resp->setBody(doc);
}
}
}
-static void updateDomainSettingsFromDocument(const DomainInfo& di, const DNSName& zonename, const Json document) {
+static void checkDefaultDNSSECAlgos() {
+ int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
+ int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
+ int k_size = arg().asNum("default-ksk-size");
+ int z_size = arg().asNum("default-zsk-size");
+
+ // Sanity check DNSSEC parameters
+ if (::arg()["default-zsk-algorithm"] != "") {
+ if (k_algo == -1)
+ throw ApiException("default-ksk-algorithm setting is set to unknown algorithm: " + ::arg()["default-ksk-algorithm"]);
+ else if (k_algo <= 10 && k_size == 0)
+ throw ApiException("default-ksk-algorithm is set to an algorithm("+::arg()["default-ksk-algorithm"]+") that requires a non-zero default-ksk-size!");
+ }
+
+ if (::arg()["default-zsk-algorithm"] != "") {
+ if (z_algo == -1)
+ throw ApiException("default-zsk-algorithm setting is set to unknown algorithm: " + ::arg()["default-zsk-algorithm"]);
+ else if (z_algo <= 10 && z_size == 0)
+ throw ApiException("default-zsk-algorithm is set to an algorithm("+::arg()["default-zsk-algorithm"]+") that requires a non-zero default-zsk-size!");
+ }
+}
+
+static void updateDomainSettingsFromDocument(UeberBackend& B, const DomainInfo& di, const DNSName& zonename, const Json document) {
string zonemaster;
+ bool shouldRectify = false;
for(auto value : document["masters"].array_items()) {
string master = value.string_value();
if (master.empty())
zonemaster += master + " ";
}
- di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
- di.backend->setMaster(zonename, zonemaster);
-
+ if (zonemaster != "") {
+ di.backend->setMaster(zonename, zonemaster);
+ }
+ if (document["kind"].is_string()) {
+ di.backend->setKind(zonename, DomainInfo::stringToKind(stringFromJson(document, "kind")));
+ }
if (document["soa_edit_api"].is_string()) {
di.backend->setDomainMetadataOne(zonename, "SOA-EDIT-API", document["soa_edit_api"].string_value());
}
if (document["soa_edit"].is_string()) {
di.backend->setDomainMetadataOne(zonename, "SOA-EDIT", document["soa_edit"].string_value());
}
+ if (document["api_rectify"].is_string()) {
+ di.backend->setDomainMetadataOne(zonename, "API-RECTIFY", document["api_rectify"].string_value());
+ }
if (document["account"].is_string()) {
di.backend->setAccount(zonename, document["account"].string_value());
}
+
+ DNSSECKeeper dk(&B);
+ bool dnssecInJSON = false;
+ bool dnssecDocVal = false;
+
+ try {
+ dnssecDocVal = boolFromJson(document, "dnssec");
+ dnssecInJSON = true;
+ }
+ catch (JsonException) {}
+
+ bool isDNSSECZone = dk.isSecuredZone(zonename);
+
+ if (dnssecInJSON) {
+ if (dnssecDocVal) {
+ if (!isDNSSECZone) {
+ checkDefaultDNSSECAlgos();
+
+ int k_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-ksk-algorithm"]);
+ int z_algo = DNSSECKeeper::shorthand2algorithm(::arg()["default-zsk-algorithm"]);
+ int k_size = arg().asNum("default-ksk-size");
+ int z_size = arg().asNum("default-zsk-size");
+
+ if (k_algo != -1) {
+ int64_t id;
+ if (!dk.addKey(zonename, true, k_algo, id, k_size)) {
+ throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
+ + "capable backends are loaded, or because the backends have DNSSEC disabled."
+ + "For the Generic SQL backends, set the 'gsqlite3-dnssec', 'gmysql-dnssec' or"
+ + "'gpgsql-dnssec' flag. Also make sure the schema has been updated for DNSSEC!");
+ }
+ }
+
+ if (z_algo != -1) {
+ int64_t id;
+ if (!dk.addKey(zonename, false, z_algo, id, z_size)) {
+ throw ApiException("No backend was able to secure '" + zonename.toString() + "', most likely because no DNSSEC"
+ + "capable backends are loaded, or because the backends have DNSSEC disabled."
+ + "For the Generic SQL backends, set the 'gsqlite3-dnssec', 'gmysql-dnssec' or"
+ + "'gpgsql-dnssec' flag. Also make sure the schema has been updated for DNSSEC!");
+ }
+ }
+
+ // Used later for NSEC3PARAM
+ isDNSSECZone = dk.isSecuredZone(zonename);
+
+ if (!isDNSSECZone) {
+ throw ApiException("Failed to secure '" + zonename.toString() + "'. Is your backend dnssec enabled? (set "
+ + "gsqlite3-dnssec, or gmysql-dnssec etc). Check this first."
+ + "If you run with the BIND backend, make sure you have configured"
+ + "it to use DNSSEC with 'bind-dnssec-db=/path/fname' and"
+ + "'pdnsutil create-bind-db /path/fname'!");
+ }
+ shouldRectify = true;
+ }
+ } else {
+ // "dnssec": false in json
+ if (isDNSSECZone) {
+ throw ApiException("Refusing to un-secure zone " + zonename.toString());
+ }
+ }
+ }
+
+ if(document["nsec3param"].string_value().length() > 0) {
+ shouldRectify = true;
+ NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
+ string error_msg = "";
+ if (!isDNSSECZone) {
+ throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"', but zone is not DNSSEC secured.");
+ }
+ if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
+ throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
+ }
+ if (!dk.setNSEC3PARAM(zonename, ns3pr, boolFromJson(document, "nsec3narrow", false))) {
+ throw ApiException("NSEC3PARAMs provided for zone '" + zonename.toString() +
+ "' passed our basic sanity checks, but cannot be used with the current backend.");
+ }
+ }
+
+ string api_rectify;
+ di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
+ if (shouldRectify && dk.isSecuredZone(zonename) && !dk.isPresigned(zonename) && api_rectify == "1") {
+ string error_msg = "";
+ if (!dk.rectifyZone(zonename, error_msg))
+ throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
+ }
}
static bool isValidMetadataKind(const string& kind, bool readonly) {
// the following options do not allow modifications via API
static vector<string> protectedOptions {
+ "API-RECTIFY",
"NSEC3NARROW",
"NSEC3PARAM",
"PRESIGNED",
checkDuplicateRecords(new_records);
+ if (boolFromJson(document, "dnssec", false)) {
+ checkDefaultDNSSECAlgos();
+
+ if(document["nsec3param"].string_value().length() > 0) {
+ NSEC3PARAMRecordContent ns3pr(document["nsec3param"].string_value());
+ string error_msg = "";
+ if (!dk.checkNSEC3PARAM(ns3pr, error_msg)) {
+ throw ApiException("NSEC3PARAMs provided for zone '"+zonename.toString()+"' are invalid. " + error_msg);
+ }
+ }
+ }
+
// no going back after this
if(!B.createDomain(zonename))
throw ApiException("Creating domain '"+zonename.toString()+"' failed");
di.backend->feedComment(c);
}
- updateDomainSettingsFromDocument(di, zonename, document);
+ updateDomainSettingsFromDocument(B, di, zonename, document);
di.backend->commitTransaction();
storeChangedPTRs(B, new_ptrs);
- fillZone(zonename, resp);
+ fillZone(zonename, resp, shouldDoRRSets(req));
resp->status = 201;
return;
}
if(!B.getDomainInfo(zonename, di))
throw ApiException("Could not find domain '"+zonename.toString()+"'");
- updateDomainSettingsFromDocument(di, zonename, req->json());
+ updateDomainSettingsFromDocument(B, di, zonename, req->json());
resp->body = "";
resp->status = 204; // No Content, but indicate success
patchZone(req, resp);
return;
} else if (req->method == "GET") {
- fillZone(zonename, resp);
+ fillZone(zonename, resp, shouldDoRRSets(req));
return;
}
di.backend->abortTransaction();
throw;
}
+
+ DNSSECKeeper dk;
+ string api_rectify;
+ di.backend->getDomainMetadataOne(zonename, "API-RECTIFY", api_rectify);
+ if (dk.isSecuredZone(zonename) && !dk.isPresigned(zonename) && api_rectify == "1") {
+ string error_msg = "";
+ if (!dk.rectifyZone(zonename, error_msg))
+ throw ApiException("Failed to rectify '" + zonename.toString() + "' " + error_msg);
+ }
+
di.backend->commitTransaction();
purgeAuthCachesExact(zonename);
headers={'content-type': 'application/json'})
self.assertEquals(r.status_code, 422)
+ def test_create_zone_with_dnssec(self):
+ """
+ Create a zone with "dnssec" set and see if a key was made.
+ """
+ name = unique_zone_name()
+ name, payload, data = self.create_zone(dnssec=True)
+
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
+
+ for k in ('dnssec', ):
+ self.assertIn(k, data)
+ if k in payload:
+ self.assertEquals(data[k], payload[k])
+
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/cryptokeys'))
+
+ keys = r.json()
+
+ print keys
+
+ self.assertEquals(r.status_code, 200)
+ self.assertEquals(len(keys), 1)
+ self.assertEquals(keys[0]['type'], 'Cryptokey')
+ self.assertEquals(keys[0]['active'], True)
+ self.assertEquals(keys[0]['keytype'], 'csk')
+
+ def test_create_zone_with_nsec3param(self):
+ """
+ Create a zone with "nsec3param" set and see if the metadata was added.
+ """
+ name = unique_zone_name()
+ nsec3param = '1 0 500 aabbccddeeff'
+ name, payload, data = self.create_zone(dnssec=True, nsec3param=nsec3param)
+
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
+
+ for k in ('dnssec', 'nsec3param'):
+ self.assertIn(k, data)
+ if k in payload:
+ self.assertEquals(data[k], payload[k])
+
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/metadata/NSEC3PARAM'))
+
+ data = r.json()
+
+ print data
+
+ self.assertEquals(r.status_code, 200)
+ self.assertEquals(len(data['metadata']), 1)
+ self.assertEquals(data['kind'], 'NSEC3PARAM')
+ self.assertEquals(data['metadata'][0], nsec3param)
+
+ def test_create_zone_with_nsec3narrow(self):
+ """
+ Create a zone with "nsec3narrow" set and see if the metadata was added.
+ """
+ name = unique_zone_name()
+ nsec3param = '1 0 500 aabbccddeeff'
+ name, payload, data = self.create_zone(dnssec=True, nsec3param=nsec3param,
+ nsec3narrow=True)
+
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
+
+ for k in ('dnssec', 'nsec3param', 'nsec3narrow'):
+ self.assertIn(k, data)
+ if k in payload:
+ self.assertEquals(data[k], payload[k])
+
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/metadata/NSEC3NARROW'))
+
+ data = r.json()
+
+ print data
+
+ self.assertEquals(r.status_code, 200)
+ self.assertEquals(len(data['metadata']), 1)
+ self.assertEquals(data['kind'], 'NSEC3NARROW')
+ self.assertEquals(data['metadata'][0], '1')
+
def test_zone_absolute_url(self):
name, payload, data = self.create_zone()
r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
# should return zone, SOA, ns1, ns2, sub.sub A (but not the ENT)
self.assertEquals(len(r.json()), 5)
+ def test_rrset_parameter_post_false(self):
+ name = unique_zone_name()
+ payload = {
+ 'name': name,
+ 'kind': 'Native',
+ 'nameservers': ['ns1.example.com.', 'ns2.example.com.']
+ }
+ r = self.session.post(
+ self.url("/api/v1/servers/localhost/zones?rrsets=false"),
+ data=json.dumps(payload),
+ headers={'content-type': 'application/json'})
+ print r.json()
+ self.assert_success_json(r)
+ self.assertEquals(r.status_code, 201)
+ self.assertEquals(r.json().get('rrsets'), None)
+
+ def test_rrset_false_parameter(self):
+ name = unique_zone_name()
+ self.create_zone(name=name, kind='Native')
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=false"))
+ self.assert_success_json(r)
+ print r.json()
+ self.assertEquals(r.json().get('rrsets'), None)
+
+ def test_rrset_true_parameter(self):
+ name = unique_zone_name()
+ self.create_zone(name=name, kind='Native')
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=true"))
+ self.assert_success_json(r)
+ print r.json()
+ self.assertEquals(len(r.json().get('rrsets')), 2)
+
+ def test_wrong_rrset_parameter(self):
+ name = unique_zone_name()
+ self.create_zone(name=name, kind='Native')
+ r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=foobar"))
+ self.assertEquals(r.status_code, 422)
+ self.assertIn("'rrsets' request parameter value 'foobar' is not supported", r.json()['error'])
+
@unittest.skipIf(not is_auth(), "Not applicable")
class AuthRootZone(ApiTestCase, AuthZonesHelperMixin):
# should return zone, SOA
self.assertEquals(len(r.json()), 2)
-
@unittest.skipIf(not is_auth(), "Not applicable")
class AuthZoneKeys(ApiTestCase, AuthZonesHelperMixin):