From c501d775bafa4b1165b8cd51c124d306e6b7f834 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 17 Jul 2023 09:49:09 -0700 Subject: [PATCH] Add support for relative target/exchange/nameserver values --- CHANGELOG.md | 1 + octodns/record/mx.py | 25 ++++++++++++++---------- octodns/record/srv.py | 23 +++++++++++++--------- octodns/record/target.py | 6 +++++- tests/test_octodns_record_mx.py | 15 +++++++++++++++ tests/test_octodns_record_srv.py | 20 +++++++++++++++++++ tests/test_octodns_record_target.py | 30 +++++++++++++++++++++++++++++ 7 files changed, 100 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c23ac..671eee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Correctly handle FQDNs in TinyDNS config files that end with trailing .'s * Complete rewrite of TinyDnsBaseSource to fully implement the spec and the ipv6 extensions +* Allow relative target, nameserver, or exchange values ## v1.0.0.rc0 - 2023-05-16 - First of the ones diff --git a/octodns/record/mx.py b/octodns/record/mx.py index 77d34a7..b72732b 100644 --- a/octodns/record/mx.py +++ b/octodns/record/mx.py @@ -45,16 +45,21 @@ class MxValue(EqualityTupleMixin, dict): reasons.append('missing exchange') continue exchange = idna_encode(exchange) - if ( - exchange != '.' - and not FQDN(exchange, allow_underscores=True).is_valid - ): - reasons.append( - f'Invalid MX exchange "{exchange}" is not ' - 'a valid FQDN.' - ) - elif not exchange.endswith('.'): - reasons.append(f'MX value "{exchange}" missing trailing .') + if '.' not in exchange: + reasons.append(f'MX exchange "{exchange}" is relative') + else: + if ( + exchange != '.' + and not FQDN(exchange, allow_underscores=True).is_valid + ): + reasons.append( + f'Invalid MX exchange "{exchange}" is not ' + 'a valid FQDN.' + ) + elif not exchange.endswith('.'): + reasons.append( + f'MX value "{exchange}" missing trailing .' + ) except KeyError: reasons.append('missing exchange') return reasons diff --git a/octodns/record/srv.py b/octodns/record/srv.py index af3797c..7ddb0f7 100644 --- a/octodns/record/srv.py +++ b/octodns/record/srv.py @@ -69,15 +69,20 @@ class SrvValue(EqualityTupleMixin, dict): reasons.append('missing target') continue target = idna_encode(target) - if not target.endswith('.'): - reasons.append(f'SRV value "{target}" missing trailing .') - if ( - target != '.' - and not FQDN(target, allow_underscores=True).is_valid - ): - reasons.append( - f'Invalid SRV target "{target}" is not a valid FQDN.' - ) + if '.' not in target: + reasons.append(f'SRV value "{target}" is relative') + else: + if not target.endswith('.'): + reasons.append( + f'SRV value "{target}" missing trailing .' + ) + if ( + target != '.' + and not FQDN(target, allow_underscores=True).is_valid + ): + reasons.append( + f'Invalid SRV target "{target}" is not a valid FQDN.' + ) except KeyError: reasons.append('missing target') return reasons diff --git a/octodns/record/target.py b/octodns/record/target.py index f9dbc18..b8f65a5 100644 --- a/octodns/record/target.py +++ b/octodns/record/target.py @@ -19,6 +19,8 @@ class _TargetValue(str): reasons.append('empty value') elif not data: reasons.append('missing value') + elif '.' not in data: + reasons.append(f'{_type} value "{data}" is relative') else: data = idna_encode(data) if not FQDN(str(data), allow_underscores=True).is_valid: @@ -58,7 +60,9 @@ class _TargetsValue(str): reasons = [] for value in data: value = idna_encode(value) - if not FQDN(value, allow_underscores=True).is_valid: + if '.' not in value: + reasons.append(f'{_type} value "{value}" is relative') + elif not FQDN(value, allow_underscores=True).is_valid: reasons.append( f'Invalid {_type} value "{value}" is not a valid FQDN.' ) diff --git a/tests/test_octodns_record_mx.py b/tests/test_octodns_record_mx.py index 1ae37e6..3594c59 100644 --- a/tests/test_octodns_record_mx.py +++ b/tests/test_octodns_record_mx.py @@ -262,3 +262,18 @@ class TestRecordMx(TestCase): }, ) self.assertEqual('.', record.values[0].exchange) + + # relative exchange + with self.assertRaises(ValidationError) as ctx: + Record.new( + self.zone, + '', + { + 'type': 'MX', + 'ttl': 600, + 'value': {'preference': 10, 'value': 'isrelative'}, + }, + ) + self.assertEqual( + ['MX exchange "isrelative" is relative'], ctx.exception.reasons + ) diff --git a/tests/test_octodns_record_srv.py b/tests/test_octodns_record_srv.py index 3cc39b5..1d94400 100644 --- a/tests/test_octodns_record_srv.py +++ b/tests/test_octodns_record_srv.py @@ -429,3 +429,23 @@ class TestRecordSrv(TestCase): ['Invalid SRV target "100 foo.bar.com." is not a valid FQDN.'], ctx.exception.reasons, ) + + # relative target + with self.assertRaises(ValidationError) as ctx: + Record.new( + self.zone, + '_srv._tcp', + { + 'type': 'SRV', + 'ttl': 600, + 'value': { + 'priority': 1, + 'weight': 2, + 'port': 3, + 'target': 'isrelative', + }, + }, + ) + self.assertEqual( + ['SRV value "isrelative" is relative'], ctx.exception.reasons + ) diff --git a/tests/test_octodns_record_target.py b/tests/test_octodns_record_target.py index 715cd4a..f69fcab 100644 --- a/tests/test_octodns_record_target.py +++ b/tests/test_octodns_record_target.py @@ -4,6 +4,7 @@ from unittest import TestCase +from octodns.record import Record, ValidationError from octodns.record.alias import AliasRecord from octodns.record.target import _TargetValue from octodns.zone import Zone @@ -28,3 +29,32 @@ class TestRecordTarget(TestCase): zone = Zone('unit.tests.', []) a = AliasRecord(zone, 'a', {'ttl': 42, 'value': 'some.target.'}) self.assertEqual('some.target.', a.value.rdata_text) + + def test_relative_target(self): + zone = Zone('unit.tests.', []) + + data = {'ttl': 43, 'type': 'CNAME', 'value': 'isrelative'} + with self.assertRaises(ValidationError) as ctx: + Record.new(zone, 'cname', data) + self.assertEqual( + ['CNAME value "isrelative" is relative'], ctx.exception.reasons + ) + cname = Record.new(zone, 'cname', data, lenient=True) + self.assertEqual(data['value'], cname.value) + + data = { + 'ttl': 43, + 'type': 'NS', + 'values': ['isrelative1', 'isrelative2'], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(zone, 'ns', data) + self.assertEqual( + [ + 'NS value "isrelative1" is relative', + 'NS value "isrelative2" is relative', + ], + ctx.exception.reasons, + ) + cname = Record.new(zone, 'ns', data, lenient=True) + self.assertEqual(data['values'], cname.values)