diff --git a/octodns/provider/dyn.py b/octodns/provider/dyn.py index 3b7b9ea..721b3a7 100644 --- a/octodns/provider/dyn.py +++ b/octodns/provider/dyn.py @@ -455,7 +455,7 @@ class DynProvider(BaseProvider): return [{ 'txtdata': v, 'ttl': record.ttl, - } for v in record.values] + } for v in record.chunked_values] def _kwargs_for_SRV(self, record): return [{ diff --git a/octodns/provider/route53.py b/octodns/provider/route53.py index 0600511..7623648 100644 --- a/octodns/provider/route53.py +++ b/octodns/provider/route53.py @@ -114,8 +114,7 @@ class _Route53Record(object): for v in record.values] def _values_for_quoted(self, record): - return ['"{}"'.format(v.replace('"', '\\"')) - for v in record.values] + return record.chunked_values _values_for_SPF = _values_for_quoted _values_for_TXT = _values_for_quoted diff --git a/octodns/record.py b/octodns/record.py index 8ef80be..554d98b 100644 --- a/octodns/record.py +++ b/octodns/record.py @@ -704,8 +704,8 @@ class SshfpRecord(_ValuesMixin, Record): _unescaped_semicolon_re = re.compile(r'\w;') -class SpfRecord(_ValuesMixin, Record): - _type = 'SPF' +class _ChunkedValuesMixin(_ValuesMixin): + CHUNK_SIZE = 255 @classmethod def _validate_value(cls, value): @@ -714,9 +714,29 @@ class SpfRecord(_ValuesMixin, Record): return [] def _process_values(self, values): + ret = [] + for v in values: + if v and v[0] == '"': + v = v[1:-1] + ret.append(v.replace('" "', '')) + return ret + + @property + def chunked_values(self): + values = [] + for v in self.values: + v = v.replace('"', '\\"') + vs = [v[i:i + self.CHUNK_SIZE] + for i in range(0, len(v), self.CHUNK_SIZE)] + vs = '" "'.join(vs) + values.append('"{}"'.format(vs)) return values +class SpfRecord(_ChunkedValuesMixin, Record): + _type = 'SPF' + + class SrvValue(object): @classmethod @@ -797,14 +817,5 @@ class SrvRecord(_ValuesMixin, Record): return [SrvValue(v) for v in values] -class TxtRecord(_ValuesMixin, Record): +class TxtRecord(_ChunkedValuesMixin, Record): _type = 'TXT' - - @classmethod - def _validate_value(cls, value): - if _unescaped_semicolon_re.search(value): - return ['unescaped ;'] - return [] - - def _process_values(self, values): - return values diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 51676a3..41b63a9 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -1490,3 +1490,59 @@ class TestRecordValidation(TestCase): 'value': 'this has some; semi-colons\; in it', }) self.assertEquals(['unescaped ;'], ctx.exception.reasons) + + def test_TXT_long_value_chunking(self): + expected = '"Lorem ipsum dolor sit amet, consectetur adipiscing ' \ + 'elit, seddo eiusmod tempor incididunt ut labore et dolore ' \ + 'magna aliqua. Ut enim ad minim veniam, quis nostrud ' \ + 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' \ + 'consequat. Duis aute irure dolor in" " reprehenderit in ' \ + 'voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' \ + 'Excepteur sint occaecat cupidatat non proident, sunt in culpa ' \ + 'qui officia deserunt mollit anim id est laborum."' + + # Single string + single = Record.new(self.zone, '', { + 'type': 'TXT', + 'ttl': 600, + 'values': [ + 'hello world', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed' + 'do eiusmod tempor incididunt ut labore et dolore magna ' + 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation ' + 'ullamco laboris nisi ut aliquip ex ea commodo consequat. ' + 'Duis aute irure dolor in reprehenderit in voluptate velit ' + 'esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ' + 'occaecat cupidatat non proident, sunt in culpa qui officia ' + 'deserunt mollit anim id est laborum.', + 'this has some\; semi-colons\; in it', + ] + }) + self.assertEquals(3, len(single.values)) + self.assertEquals(3, len(single.chunked_values)) + # Note we are checking that this normalizes the chunking, not that we + # get out what we put in. + self.assertEquals(expected, single.chunked_values[0]) + + # Chunked + chunked = Record.new(self.zone, '', { + 'type': 'TXT', + 'ttl': 600, + 'values': [ + '"hello world"', + '"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed' + 'do eiusmod tempor incididunt ut labore et dolore magna ' + 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation ' + 'ullamco laboris nisi ut aliquip ex" " ea commodo consequat. ' + 'Duis aute irure dolor in reprehenderit in voluptate velit ' + 'esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ' + 'occaecat cupidatat non proident, sunt in culpa qui officia ' + 'deserunt mollit anim id est laborum."', + '"this has some\; semi-colons\; in it"', + ] + }) + self.assertEquals(expected, chunked.chunked_values[0]) + # should be single values, no quoting + self.assertEquals(single.values, chunked.values) + # should be chunked values, with quoting + self.assertEquals(single.chunked_values, chunked.chunked_values)