Source code for octodns.record.naptr

#
#
#

from ..equality import EqualityTupleMixin
from .base import Record, ValuesMixin, unquote
from .rr import RrParseError
from .target import _check_target_trailing_dot
from .validator import ValueValidator


[docs] class NaptrValueValidator(ValueValidator): ''' Validates NAPTR rdata: ``order`` and ``preference`` are integer-parsable, ``flags`` is one of the RFC 3403 values, and ``service``, ``regexp``, and ``replacement`` are all present. '''
[docs] def validate(self, value_cls, data, _type): reasons = [] for value in data: try: int(value['order']) except KeyError: reasons.append('missing order') except ValueError: reasons.append(f'invalid order "{value["order"]}"') try: int(value['preference']) except KeyError: reasons.append('missing preference') except ValueError: reasons.append(f'invalid preference "{value["preference"]}"') try: flags = value['flags'] if flags not in value_cls.VALID_FLAGS: reasons.append(f'unrecognized flags "{flags}"') except KeyError: reasons.append('missing flags') # TODO: validate these... they're non-trivial for k in ('service', 'regexp', 'replacement'): if k not in value: reasons.append(f'missing {k}') return reasons
[docs] class NaptrValueRfcValidator(ValueValidator): ''' Strict NAPTR rdata validator per RFC 3403 §4.1. - ``order`` and ``preference`` must be integers in [0, 65535] (uint16). - ``flags`` must be one of ``S``, ``A``, ``U``, ``P`` (case-insensitive) or empty. - ``replacement`` must be a fully-qualified domain name (ending with ``"."``) or ``"."`` for the null replacement. Enabled as part of the ``strict`` validator set:: manager: enabled: - strict ''' _valid_flags = frozenset('SAUP')
[docs] def validate(self, value_cls, data, _type): reasons = [] for value in data: for field in ('order', 'preference'): if field not in value: reasons.append(f'missing {field}') else: try: int_val = int(value[field]) if not 0 <= int_val <= 65535: reasons.append( f'invalid {field} "{int_val}"; must be 0-65535' ) except (ValueError, TypeError): reasons.append(f'invalid {field} "{value[field]}"') if 'flags' not in value: reasons.append('missing flags') else: flags = value['flags'] if flags and flags.upper() not in self._valid_flags: reasons.append(f'unrecognized flags "{flags}"') for k in ('service', 'regexp'): if k not in value: reasons.append(f'missing {k}') if 'replacement' not in value: reasons.append('missing replacement') return reasons
[docs] class NaptrValueBestPracticeValidator(ValueValidator): ''' Checks that the NAPTR ``replacement`` field ends with a trailing ``.`` (fully-qualified name) when non-empty and not the null replacement ``"."``. Enabled as part of the ``best-practice`` validator set:: manager: enabled: - best-practice '''
[docs] def validate(self, value_cls, data, _type): reasons = [] for value in data: replacement = value.get('replacement') if replacement: reasons += _check_target_trailing_dot( replacement, _type, 'replacement' ) return reasons
[docs] class NaptrValue(EqualityTupleMixin, dict): VALID_FLAGS = ('S', 'A', 'U', 'P', 's', 'a', 'u', 'p') VALIDATORS = [ NaptrValueValidator('naptr-value', sets={'legacy'}), NaptrValueRfcValidator('naptr-value-rfc', sets={'strict'}), NaptrValueBestPracticeValidator( 'naptr-value-best-practice', sets={'best-practice'} ), ]
[docs] @classmethod def _schema(cls): return { 'type': 'object', 'required': [ 'order', 'preference', 'flags', 'service', 'regexp', 'replacement', ], 'properties': { 'order': {'type': 'integer', 'minimum': 0, 'maximum': 65535}, 'preference': { 'type': 'integer', 'minimum': 0, 'maximum': 65535, }, 'flags': {'enum': list(cls.VALID_FLAGS)}, 'service': {'type': 'string'}, 'regexp': {'type': 'string'}, 'replacement': {'type': 'string'}, }, }
[docs] @classmethod def parse_rdata_text(cls, value): try: order, preference, flags, service, regexp, replacement = ( value.split(' ') ) except ValueError: raise RrParseError() try: order = int(order) preference = int(preference) except ValueError: pass flags = unquote(flags) service = unquote(service) regexp = unquote(regexp) replacement = unquote(replacement) return { 'order': order, 'preference': preference, 'flags': flags, 'service': service, 'regexp': regexp, 'replacement': replacement, }
[docs] @classmethod def process(cls, values): return [cls(v) for v in values]
[docs] def __init__(self, value): super().__init__( { 'order': int(value['order']), 'preference': int(value['preference']), 'flags': value['flags'].upper(), 'service': value['service'], 'regexp': value['regexp'], 'replacement': value['replacement'], } )
@property def order(self): return self['order'] @order.setter def order(self, value): self['order'] = value @property def preference(self): return self['preference'] @preference.setter def preference(self, value): self['preference'] = value @property def flags(self): return self['flags'] @flags.setter def flags(self, value): self['flags'] = value @property def service(self): return self['service'] @service.setter def service(self, value): self['service'] = value @property def regexp(self): return self['regexp'] @regexp.setter def regexp(self, value): self['regexp'] = value @property def replacement(self): return self['replacement'] @replacement.setter def replacement(self, value): self['replacement'] = value @property def data(self): return self @property def rdata_text(self): # RFC 3403 requires flags, service, and regexp to be quoted character-strings flags = self.flags or '' service = self.service or '' regexp = self.regexp or '' return f'{self.order} {self.preference} "{flags}" "{service}" "{regexp}" {self.replacement}'
[docs] def template(self, params): if ( '{' not in self.service and '{' not in self.regexp and '{' not in self.replacement ): return self new = self.__class__(self) new.service = new.service.format(**params) new.regexp = new.regexp.format(**params) new.replacement = new.replacement.format(**params) return new
def __hash__(self): return hash(self.__repr__())
[docs] def _equality_tuple(self): return ( self.order, self.preference, self.flags, self.service, self.regexp, self.replacement, )
[docs] def __repr__(self): flags = self.flags if self.flags is not None else '' service = self.service if self.service is not None else '' regexp = self.regexp if self.regexp is not None else '' return ( f"'{self.order} {self.preference} \"{flags}\" \"{service}\" " f"\"{regexp}\" {self.replacement}'" )
[docs] class NaptrRecord(ValuesMixin, Record): REFERENCES = ( 'https://datatracker.ietf.org/doc/html/rfc3401', 'https://datatracker.ietf.org/doc/html/rfc3402', 'https://datatracker.ietf.org/doc/html/rfc3403', 'https://datatracker.ietf.org/doc/html/rfc3404', 'https://datatracker.ietf.org/doc/html/rfc3405', ) _type = 'NAPTR' _value_type = NaptrValue
Record.register_type(NaptrRecord)