Display of records in various :doc:`pdnsutil <manpages/pdnsutil.1>` commands
will now always contain explicit trailing dots, for consistency.
+Record handling in the API
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Record data sent through the API will now be normalized to be closer to their
+actual zone representation.
+As a result of this change, reading back these records may show a different
+representation than expected.
+
4.9.0 to 5.0.0
--------------
}
}
+// Clean and unescape a record content string, in order to minimize the
+// risk of mismatch between it and its canonical form returned by
+// makeApiRecordContent().
+// To do so, we remove leading and trailing whitespace, and perform
+// RFC1035 processing on the data until all the chunks have been processed.
+static std::string normalizeJsonString(const std::string& jsonContent)
+{
+ std::ostringstream ret;
+
+ std::string copy{jsonContent};
+ // Trim surrounding whitespace
+ boost::trim_right(copy);
+ boost::trim_left(copy);
+
+ std::string_view input{copy};
+ auto len = input.size();
+ size_t pos = 0;
+ while (pos < len) {
+ std::string chunk;
+ // Preserve quotes in the result if the chunk is quoted.
+ bool quote = input[pos] == '"';
+ auto chunksize = parseRFC1035CharString(input.substr(pos), chunk);
+ if (quote) {
+ ret << '"';
+ }
+ // We would love to simply feed chunk to ret here, but unfortunately
+ // we need to RFC1035 escape non-printable characters again.
+ for (char chr : chunk) {
+ if (chr >= 0x20 && chr < 0x7f) {
+ ret << chr;
+ }
+ else {
+ ret << '\\' << std::setfill('0') << std::setw(3) << static_cast<unsigned int>(chr) << std::setw(0);
+ }
+ }
+ if (quote) {
+ ret << '"';
+ }
+ pos += chunksize;
+ // Keep only one space for space-separated chunks.
+ if (pos < len && std::isspace(static_cast<unsigned char>(input[pos])) != 0) {
+ while (pos < len && std::isspace(static_cast<unsigned char>(input[pos])) != 0) {
+ ++pos;
+ }
+ ret << ' ';
+ }
+ }
+ return ret.str();
+}
+
static void gatherRecords(const Json& container, const DNSName& qname, const QType& qtype, const uint32_t ttl, vector<DNSResourceRecord>& new_records)
{
DNSResourceRecord resourceRecord;
validateGatheredRRType(resourceRecord);
const auto& items = container["records"].array_items();
for (const auto& record : items) {
- string content = stringFromJson(record, "content");
+ string content = normalizeJsonString(stringFromJson(record, "content"));
if (record.object_items().count("priority") > 0) {
throw std::runtime_error("`priority` element is not allowed in record");
}
data=json.dumps(payload),
headers={'content-type': 'application/json'})
self.assertEqual(r.status_code, 422)
- self.assertIn('contains an invalid escape', r.json()['error'])
+ self.assertIn('Data field in DNS should start with quote (") at position 9', r.json()['error'])
def test_zone_rr_update_with_escapes(self):
name, payload, zone = self.create_zone()
rrset = {
'changetype': 'replace',
'name': name,
- 'type': 'FAFAFA',
+ 'type': 'FAFAFAFA', # obviously a FIPv6 address
'ttl': 3600,
'records': [
{
# self.assertIn('You cannot have record(s) under CNAME/DNAME', r.json()['error'])
def test_create_zone_with_leading_space(self):
- # Actual regression.
name, payload, zone = self.create_zone()
rrset = {
'changetype': 'replace',
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.assertEqual(r.status_code, 422)
- self.assertIn('Not in expected format', r.json()['error'])
+ self.assert_success(r)
@unittest.skipIf(is_auth_lmdb(), "No out-of-zone storage in LMDB")
def test_zone_rr_delete_out_of_zone(self):