| @ -0,0 +1,101 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class CaaValue(EqualityTupleMixin, dict): | |||||
| # https://tools.ietf.org/html/rfc6844#page-5 | |||||
| @classmethod | |||||
| def parse_rdata_text(cls, value): | |||||
| try: | |||||
| flags, tag, value = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| flags = int(flags) | |||||
| except ValueError: | |||||
| pass | |||||
| return {'flags': flags, 'tag': tag, 'value': value} | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| try: | |||||
| flags = int(value.get('flags', 0)) | |||||
| if flags < 0 or flags > 255: | |||||
| reasons.append(f'invalid flags "{flags}"') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid flags "{value["flags"]}"') | |||||
| if 'tag' not in value: | |||||
| reasons.append('missing tag') | |||||
| if 'value' not in value: | |||||
| reasons.append('missing value') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'flags': int(value.get('flags', 0)), | |||||
| 'tag': value['tag'], | |||||
| 'value': value['value'], | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def flags(self): | |||||
| return self['flags'] | |||||
| @flags.setter | |||||
| def flags(self, value): | |||||
| self['flags'] = value | |||||
| @property | |||||
| def tag(self): | |||||
| return self['tag'] | |||||
| @tag.setter | |||||
| def tag(self, value): | |||||
| self['tag'] = value | |||||
| @property | |||||
| def value(self): | |||||
| return self['value'] | |||||
| @value.setter | |||||
| def value(self, value): | |||||
| self['value'] = value | |||||
| @property | |||||
| def data(self): | |||||
| return self | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return f'{self.flags} {self.tag} {self.value}' | |||||
| def _equality_tuple(self): | |||||
| return (self.flags, self.tag, self.value) | |||||
| def __repr__(self): | |||||
| return f'{self.flags} {self.tag} "{self.value}"' | |||||
| class CaaRecord(ValuesMixin, Record): | |||||
| _type = 'CAA' | |||||
| _value_type = CaaValue | |||||
| Record.register_type(CaaRecord) | |||||
| @ -0,0 +1,83 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from .base import Record, ValuesMixin | |||||
| import re | |||||
| class _ChunkedValuesMixin(ValuesMixin): | |||||
| CHUNK_SIZE = 255 | |||||
| _unescaped_semicolon_re = re.compile(r'\w;') | |||||
| def chunked_value(self, value): | |||||
| value = value.replace('"', '\\"') | |||||
| vs = [ | |||||
| value[i : i + self.CHUNK_SIZE] | |||||
| for i in range(0, len(value), self.CHUNK_SIZE) | |||||
| ] | |||||
| vs = '" "'.join(vs) | |||||
| return f'"{vs}"' | |||||
| @property | |||||
| def chunked_values(self): | |||||
| values = [] | |||||
| for v in self.values: | |||||
| values.append(self.chunked_value(v)) | |||||
| return values | |||||
| class _ChunkedValue(str): | |||||
| _unescaped_semicolon_re = re.compile(r'\w;') | |||||
| @classmethod | |||||
| def parse_rdata_text(cls, value): | |||||
| try: | |||||
| return value.replace(';', '\\;') | |||||
| except AttributeError: | |||||
| return value | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not data: | |||||
| return ['missing value(s)'] | |||||
| elif not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| if cls._unescaped_semicolon_re.search(value): | |||||
| reasons.append(f'unescaped ; in "{value}"') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| ret = [] | |||||
| for v in values: | |||||
| if v and v[0] == '"': | |||||
| v = v[1:-1] | |||||
| ret.append(cls(v.replace('" "', ''))) | |||||
| return ret | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return self | |||||
| class SpfRecord(_ChunkedValuesMixin, Record): | |||||
| _type = 'SPF' | |||||
| _value_type = _ChunkedValue | |||||
| Record.register_type(SpfRecord) | |||||
| class TxtValue(_ChunkedValue): | |||||
| pass | |||||
| class TxtRecord(_ChunkedValuesMixin, Record): | |||||
| _type = 'TXT' | |||||
| _value_type = TxtValue | |||||
| Record.register_type(TxtRecord) | |||||
| @ -0,0 +1,136 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class DsValue(EqualityTupleMixin, dict): | |||||
| # https://www.rfc-editor.org/rfc/rfc4034.html#section-2.1 | |||||
| @classmethod | |||||
| def parse_rdata_text(cls, value): | |||||
| try: | |||||
| flags, protocol, algorithm, public_key = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| flags = int(flags) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| protocol = int(protocol) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| algorithm = int(algorithm) | |||||
| except ValueError: | |||||
| pass | |||||
| return { | |||||
| 'flags': flags, | |||||
| 'protocol': protocol, | |||||
| 'algorithm': algorithm, | |||||
| 'public_key': public_key, | |||||
| } | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| try: | |||||
| int(value['flags']) | |||||
| except KeyError: | |||||
| reasons.append('missing flags') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid flags "{value["flags"]}"') | |||||
| try: | |||||
| int(value['protocol']) | |||||
| except KeyError: | |||||
| reasons.append('missing protocol') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid protocol "{value["protocol"]}"') | |||||
| try: | |||||
| int(value['algorithm']) | |||||
| except KeyError: | |||||
| reasons.append('missing algorithm') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid algorithm "{value["algorithm"]}"') | |||||
| if 'public_key' not in value: | |||||
| reasons.append('missing public_key') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'flags': int(value['flags']), | |||||
| 'protocol': int(value['protocol']), | |||||
| 'algorithm': int(value['algorithm']), | |||||
| 'public_key': value['public_key'], | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def flags(self): | |||||
| return self['flags'] | |||||
| @flags.setter | |||||
| def flags(self, value): | |||||
| self['flags'] = value | |||||
| @property | |||||
| def protocol(self): | |||||
| return self['protocol'] | |||||
| @protocol.setter | |||||
| def protocol(self, value): | |||||
| self['protocol'] = value | |||||
| @property | |||||
| def algorithm(self): | |||||
| return self['algorithm'] | |||||
| @algorithm.setter | |||||
| def algorithm(self, value): | |||||
| self['algorithm'] = value | |||||
| @property | |||||
| def public_key(self): | |||||
| return self['public_key'] | |||||
| @public_key.setter | |||||
| def public_key(self, value): | |||||
| self['public_key'] = value | |||||
| @property | |||||
| def data(self): | |||||
| return self | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return ( | |||||
| f'{self.flags} {self.protocol} {self.algorithm} {self.public_key}' | |||||
| ) | |||||
| def _equality_tuple(self): | |||||
| return (self.flags, self.protocol, self.algorithm, self.public_key) | |||||
| def __repr__(self): | |||||
| return ( | |||||
| f'{self.flags} {self.protocol} {self.algorithm} {self.public_key}' | |||||
| ) | |||||
| class DsRecord(ValuesMixin, Record): | |||||
| _type = 'DS' | |||||
| _value_type = DsValue | |||||
| Record.register_type(DsRecord) | |||||
| @ -0,0 +1,358 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class LocValue(EqualityTupleMixin, dict): | |||||
| # TODO: this does not really match the RFC, but it's stuck using the details | |||||
| # of how the type was impelemented. Would be nice to rework things to match | |||||
| # while maintaining backwards compatibility. | |||||
| # https://www.rfc-editor.org/rfc/rfc1876.html | |||||
| @classmethod | |||||
| def parse_rdata_text(cls, value): | |||||
| try: | |||||
| value = value.replace('m', '') | |||||
| ( | |||||
| lat_degrees, | |||||
| lat_minutes, | |||||
| lat_seconds, | |||||
| lat_direction, | |||||
| long_degrees, | |||||
| long_minutes, | |||||
| long_seconds, | |||||
| long_direction, | |||||
| altitude, | |||||
| size, | |||||
| precision_horz, | |||||
| precision_vert, | |||||
| ) = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| lat_degrees = int(lat_degrees) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| lat_minutes = int(lat_minutes) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| long_degrees = int(long_degrees) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| long_minutes = int(long_minutes) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| lat_seconds = float(lat_seconds) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| long_seconds = float(long_seconds) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| altitude = float(altitude) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| size = float(size) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| precision_horz = float(precision_horz) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| precision_vert = float(precision_vert) | |||||
| except ValueError: | |||||
| pass | |||||
| return { | |||||
| 'lat_degrees': lat_degrees, | |||||
| 'lat_minutes': lat_minutes, | |||||
| 'lat_seconds': lat_seconds, | |||||
| 'lat_direction': lat_direction, | |||||
| 'long_degrees': long_degrees, | |||||
| 'long_minutes': long_minutes, | |||||
| 'long_seconds': long_seconds, | |||||
| 'long_direction': long_direction, | |||||
| 'altitude': altitude, | |||||
| 'size': size, | |||||
| 'precision_horz': precision_horz, | |||||
| 'precision_vert': precision_vert, | |||||
| } | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| int_keys = [ | |||||
| 'lat_degrees', | |||||
| 'lat_minutes', | |||||
| 'long_degrees', | |||||
| 'long_minutes', | |||||
| ] | |||||
| float_keys = [ | |||||
| 'lat_seconds', | |||||
| 'long_seconds', | |||||
| 'altitude', | |||||
| 'size', | |||||
| 'precision_horz', | |||||
| 'precision_vert', | |||||
| ] | |||||
| direction_keys = ['lat_direction', 'long_direction'] | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| for key in int_keys: | |||||
| try: | |||||
| int(value[key]) | |||||
| if ( | |||||
| ( | |||||
| key == 'lat_degrees' | |||||
| and not 0 <= int(value[key]) <= 90 | |||||
| ) | |||||
| or ( | |||||
| key == 'long_degrees' | |||||
| and not 0 <= int(value[key]) <= 180 | |||||
| ) | |||||
| or ( | |||||
| key in ['lat_minutes', 'long_minutes'] | |||||
| and not 0 <= int(value[key]) <= 59 | |||||
| ) | |||||
| ): | |||||
| reasons.append( | |||||
| f'invalid value for {key} ' f'"{value[key]}"' | |||||
| ) | |||||
| except KeyError: | |||||
| reasons.append(f'missing {key}') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid {key} "{value[key]}"') | |||||
| for key in float_keys: | |||||
| try: | |||||
| float(value[key]) | |||||
| if ( | |||||
| ( | |||||
| key in ['lat_seconds', 'long_seconds'] | |||||
| and not 0 <= float(value[key]) <= 59.999 | |||||
| ) | |||||
| or ( | |||||
| key == 'altitude' | |||||
| and not -100000.00 | |||||
| <= float(value[key]) | |||||
| <= 42849672.95 | |||||
| ) | |||||
| or ( | |||||
| key in ['size', 'precision_horz', 'precision_vert'] | |||||
| and not 0 <= float(value[key]) <= 90000000.00 | |||||
| ) | |||||
| ): | |||||
| reasons.append( | |||||
| f'invalid value for {key} ' f'"{value[key]}"' | |||||
| ) | |||||
| except KeyError: | |||||
| reasons.append(f'missing {key}') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid {key} "{value[key]}"') | |||||
| for key in direction_keys: | |||||
| try: | |||||
| str(value[key]) | |||||
| if key == 'lat_direction' and value[key] not in ['N', 'S']: | |||||
| reasons.append( | |||||
| f'invalid direction for {key} ' f'"{value[key]}"' | |||||
| ) | |||||
| if key == 'long_direction' and value[key] not in ['E', 'W']: | |||||
| reasons.append( | |||||
| f'invalid direction for {key} ' f'"{value[key]}"' | |||||
| ) | |||||
| except KeyError: | |||||
| reasons.append(f'missing {key}') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'lat_degrees': int(value['lat_degrees']), | |||||
| 'lat_minutes': int(value['lat_minutes']), | |||||
| 'lat_seconds': float(value['lat_seconds']), | |||||
| 'lat_direction': value['lat_direction'].upper(), | |||||
| 'long_degrees': int(value['long_degrees']), | |||||
| 'long_minutes': int(value['long_minutes']), | |||||
| 'long_seconds': float(value['long_seconds']), | |||||
| 'long_direction': value['long_direction'].upper(), | |||||
| 'altitude': float(value['altitude']), | |||||
| 'size': float(value['size']), | |||||
| 'precision_horz': float(value['precision_horz']), | |||||
| 'precision_vert': float(value['precision_vert']), | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def lat_degrees(self): | |||||
| return self['lat_degrees'] | |||||
| @lat_degrees.setter | |||||
| def lat_degrees(self, value): | |||||
| self['lat_degrees'] = value | |||||
| @property | |||||
| def lat_minutes(self): | |||||
| return self['lat_minutes'] | |||||
| @lat_minutes.setter | |||||
| def lat_minutes(self, value): | |||||
| self['lat_minutes'] = value | |||||
| @property | |||||
| def lat_seconds(self): | |||||
| return self['lat_seconds'] | |||||
| @lat_seconds.setter | |||||
| def lat_seconds(self, value): | |||||
| self['lat_seconds'] = value | |||||
| @property | |||||
| def lat_direction(self): | |||||
| return self['lat_direction'] | |||||
| @lat_direction.setter | |||||
| def lat_direction(self, value): | |||||
| self['lat_direction'] = value | |||||
| @property | |||||
| def long_degrees(self): | |||||
| return self['long_degrees'] | |||||
| @long_degrees.setter | |||||
| def long_degrees(self, value): | |||||
| self['long_degrees'] = value | |||||
| @property | |||||
| def long_minutes(self): | |||||
| return self['long_minutes'] | |||||
| @long_minutes.setter | |||||
| def long_minutes(self, value): | |||||
| self['long_minutes'] = value | |||||
| @property | |||||
| def long_seconds(self): | |||||
| return self['long_seconds'] | |||||
| @long_seconds.setter | |||||
| def long_seconds(self, value): | |||||
| self['long_seconds'] = value | |||||
| @property | |||||
| def long_direction(self): | |||||
| return self['long_direction'] | |||||
| @long_direction.setter | |||||
| def long_direction(self, value): | |||||
| self['long_direction'] = value | |||||
| @property | |||||
| def altitude(self): | |||||
| return self['altitude'] | |||||
| @altitude.setter | |||||
| def altitude(self, value): | |||||
| self['altitude'] = value | |||||
| @property | |||||
| def size(self): | |||||
| return self['size'] | |||||
| @size.setter | |||||
| def size(self, value): | |||||
| self['size'] = value | |||||
| @property | |||||
| def precision_horz(self): | |||||
| return self['precision_horz'] | |||||
| @precision_horz.setter | |||||
| def precision_horz(self, value): | |||||
| self['precision_horz'] = value | |||||
| @property | |||||
| def precision_vert(self): | |||||
| return self['precision_vert'] | |||||
| @precision_vert.setter | |||||
| def precision_vert(self, value): | |||||
| self['precision_vert'] = value | |||||
| @property | |||||
| def data(self): | |||||
| return self | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return f'{self.lat_degrees} {self.lat_minutes} {self.lat_seconds} {self.lat_direction} {self.long_degrees} {self.long_minutes} {self.long_seconds} {self.long_direction} {self.altitude}m {self.size}m {self.precision_horz}m {self.precision_vert}m' | |||||
| def __hash__(self): | |||||
| return hash( | |||||
| ( | |||||
| self.lat_degrees, | |||||
| self.lat_minutes, | |||||
| self.lat_seconds, | |||||
| self.lat_direction, | |||||
| self.long_degrees, | |||||
| self.long_minutes, | |||||
| self.long_seconds, | |||||
| self.long_direction, | |||||
| self.altitude, | |||||
| self.size, | |||||
| self.precision_horz, | |||||
| self.precision_vert, | |||||
| ) | |||||
| ) | |||||
| def _equality_tuple(self): | |||||
| return ( | |||||
| self.lat_degrees, | |||||
| self.lat_minutes, | |||||
| self.lat_seconds, | |||||
| self.lat_direction, | |||||
| self.long_degrees, | |||||
| self.long_minutes, | |||||
| self.long_seconds, | |||||
| self.long_direction, | |||||
| self.altitude, | |||||
| self.size, | |||||
| self.precision_horz, | |||||
| self.precision_vert, | |||||
| ) | |||||
| def __repr__(self): | |||||
| return ( | |||||
| f"'{self.lat_degrees} {self.lat_minutes} " | |||||
| f"{self.lat_seconds:.3f} {self.lat_direction} " | |||||
| f"{self.long_degrees} {self.long_minutes} " | |||||
| f"{self.long_seconds:.3f} {self.long_direction} " | |||||
| f"{self.altitude:.2f}m {self.size:.2f}m " | |||||
| f"{self.precision_horz:.2f}m {self.precision_vert:.2f}m'" | |||||
| ) | |||||
| class LocRecord(ValuesMixin, Record): | |||||
| _type = 'LOC' | |||||
| _value_type = LocValue | |||||
| Record.register_type(LocRecord) | |||||
| @ -0,0 +1,120 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from fqdn import FQDN | |||||
| from ..equality import EqualityTupleMixin | |||||
| from ..idna import idna_encode | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class MxValue(EqualityTupleMixin, dict): | |||||
| @classmethod | |||||
| def parse_rdata_text(cls, value): | |||||
| try: | |||||
| preference, exchange = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| preference = int(preference) | |||||
| except ValueError: | |||||
| pass | |||||
| return {'preference': preference, 'exchange': exchange} | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| try: | |||||
| try: | |||||
| int(value['preference']) | |||||
| except KeyError: | |||||
| int(value['priority']) | |||||
| except KeyError: | |||||
| reasons.append('missing preference') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid preference "{value["preference"]}"') | |||||
| exchange = None | |||||
| try: | |||||
| exchange = value.get('exchange', None) or value['value'] | |||||
| if not exchange: | |||||
| 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 .') | |||||
| except KeyError: | |||||
| reasons.append('missing exchange') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| # RFC1035 says preference, half the providers use priority | |||||
| try: | |||||
| preference = value['preference'] | |||||
| except KeyError: | |||||
| preference = value['priority'] | |||||
| # UNTIL 1.0 remove value fallback | |||||
| try: | |||||
| exchange = value['exchange'] | |||||
| except KeyError: | |||||
| exchange = value['value'] | |||||
| super().__init__( | |||||
| {'preference': int(preference), 'exchange': idna_encode(exchange)} | |||||
| ) | |||||
| @property | |||||
| def preference(self): | |||||
| return self['preference'] | |||||
| @preference.setter | |||||
| def preference(self, value): | |||||
| self['preference'] = value | |||||
| @property | |||||
| def exchange(self): | |||||
| return self['exchange'] | |||||
| @exchange.setter | |||||
| def exchange(self, value): | |||||
| self['exchange'] = value | |||||
| @property | |||||
| def data(self): | |||||
| return self | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return f'{self.preference} {self.exchange}' | |||||
| def __hash__(self): | |||||
| return hash((self.preference, self.exchange)) | |||||
| def _equality_tuple(self): | |||||
| return (self.preference, self.exchange) | |||||
| def __repr__(self): | |||||
| return f"'{self.preference} {self.exchange}'" | |||||
| class MxRecord(ValuesMixin, Record): | |||||
| _type = 'MX' | |||||
| _value_type = MxValue | |||||
| Record.register_type(MxRecord) | |||||
| @ -0,0 +1,172 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class NaptrValue(EqualityTupleMixin, dict): | |||||
| VALID_FLAGS = ('S', 'A', 'U', 'P') | |||||
| @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 | |||||
| return { | |||||
| 'order': order, | |||||
| 'preference': preference, | |||||
| 'flags': flags, | |||||
| 'service': service, | |||||
| 'regexp': regexp, | |||||
| 'replacement': replacement, | |||||
| } | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| 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 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 | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'order': int(value['order']), | |||||
| 'preference': int(value['preference']), | |||||
| 'flags': value['flags'], | |||||
| '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): | |||||
| return f'{self.order} {self.preference} {self.flags} {self.service} {self.regexp} {self.replacement}' | |||||
| def __hash__(self): | |||||
| return hash(self.__repr__()) | |||||
| def _equality_tuple(self): | |||||
| return ( | |||||
| self.order, | |||||
| self.preference, | |||||
| self.flags, | |||||
| self.service, | |||||
| self.regexp, | |||||
| self.replacement, | |||||
| ) | |||||
| 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}'" | |||||
| ) | |||||
| class NaptrRecord(ValuesMixin, Record): | |||||
| _type = 'NAPTR' | |||||
| _value_type = NaptrValue | |||||
| Record.register_type(NaptrRecord) | |||||
| @ -0,0 +1,158 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from fqdn import FQDN | |||||
| import re | |||||
| from ..equality import EqualityTupleMixin | |||||
| from ..idna import idna_encode | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class SrvValue(EqualityTupleMixin, dict): | |||||
| @classmethod | |||||
| def parse_rdata_text(self, value): | |||||
| try: | |||||
| priority, weight, port, target = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| priority = int(priority) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| weight = int(weight) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| port = int(port) | |||||
| except ValueError: | |||||
| pass | |||||
| return { | |||||
| 'priority': priority, | |||||
| 'weight': weight, | |||||
| 'port': port, | |||||
| 'target': target, | |||||
| } | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| # TODO: validate algorithm and fingerprint_type values | |||||
| try: | |||||
| int(value['priority']) | |||||
| except KeyError: | |||||
| reasons.append('missing priority') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid priority "{value["priority"]}"') | |||||
| try: | |||||
| int(value['weight']) | |||||
| except KeyError: | |||||
| reasons.append('missing weight') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid weight "{value["weight"]}"') | |||||
| try: | |||||
| int(value['port']) | |||||
| except KeyError: | |||||
| reasons.append('missing port') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid port "{value["port"]}"') | |||||
| try: | |||||
| target = value['target'] | |||||
| if not target: | |||||
| 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.' | |||||
| ) | |||||
| except KeyError: | |||||
| reasons.append('missing target') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'priority': int(value['priority']), | |||||
| 'weight': int(value['weight']), | |||||
| 'port': int(value['port']), | |||||
| 'target': idna_encode(value['target']), | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def priority(self): | |||||
| return self['priority'] | |||||
| @priority.setter | |||||
| def priority(self, value): | |||||
| self['priority'] = value | |||||
| @property | |||||
| def weight(self): | |||||
| return self['weight'] | |||||
| @weight.setter | |||||
| def weight(self, value): | |||||
| self['weight'] = value | |||||
| @property | |||||
| def port(self): | |||||
| return self['port'] | |||||
| @port.setter | |||||
| def port(self, value): | |||||
| self['port'] = value | |||||
| @property | |||||
| def target(self): | |||||
| return self['target'] | |||||
| @target.setter | |||||
| def target(self, value): | |||||
| self['target'] = value | |||||
| @property | |||||
| def data(self): | |||||
| return self | |||||
| def __hash__(self): | |||||
| return hash(self.__repr__()) | |||||
| def _equality_tuple(self): | |||||
| return (self.priority, self.weight, self.port, self.target) | |||||
| def __repr__(self): | |||||
| return f"'{self.priority} {self.weight} {self.port} {self.target}'" | |||||
| class SrvRecord(ValuesMixin, Record): | |||||
| _type = 'SRV' | |||||
| _value_type = SrvValue | |||||
| _name_re = re.compile(r'^(\*|_[^\.]+)\.[^\.]+') | |||||
| @classmethod | |||||
| def validate(cls, name, fqdn, data): | |||||
| reasons = [] | |||||
| if not cls._name_re.match(name): | |||||
| reasons.append('invalid name for SRV record') | |||||
| reasons.extend(super().validate(name, fqdn, data)) | |||||
| return reasons | |||||
| Record.register_type(SrvRecord) | |||||
| @ -0,0 +1,124 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class SshfpValue(EqualityTupleMixin, dict): | |||||
| VALID_ALGORITHMS = (1, 2, 3, 4) | |||||
| VALID_FINGERPRINT_TYPES = (1, 2) | |||||
| @classmethod | |||||
| def parse_rdata_text(self, value): | |||||
| try: | |||||
| algorithm, fingerprint_type, fingerprint = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| algorithm = int(algorithm) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| fingerprint_type = int(fingerprint_type) | |||||
| except ValueError: | |||||
| pass | |||||
| return { | |||||
| 'algorithm': algorithm, | |||||
| 'fingerprint_type': fingerprint_type, | |||||
| 'fingerprint': fingerprint, | |||||
| } | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| try: | |||||
| algorithm = int(value['algorithm']) | |||||
| if algorithm not in cls.VALID_ALGORITHMS: | |||||
| reasons.append(f'unrecognized algorithm "{algorithm}"') | |||||
| except KeyError: | |||||
| reasons.append('missing algorithm') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid algorithm "{value["algorithm"]}"') | |||||
| try: | |||||
| fingerprint_type = int(value['fingerprint_type']) | |||||
| if fingerprint_type not in cls.VALID_FINGERPRINT_TYPES: | |||||
| reasons.append( | |||||
| 'unrecognized fingerprint_type ' f'"{fingerprint_type}"' | |||||
| ) | |||||
| except KeyError: | |||||
| reasons.append('missing fingerprint_type') | |||||
| except ValueError: | |||||
| reasons.append( | |||||
| 'invalid fingerprint_type ' f'"{value["fingerprint_type"]}"' | |||||
| ) | |||||
| if 'fingerprint' not in value: | |||||
| reasons.append('missing fingerprint') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'algorithm': int(value['algorithm']), | |||||
| 'fingerprint_type': int(value['fingerprint_type']), | |||||
| 'fingerprint': value['fingerprint'], | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def algorithm(self): | |||||
| return self['algorithm'] | |||||
| @algorithm.setter | |||||
| def algorithm(self, value): | |||||
| self['algorithm'] = value | |||||
| @property | |||||
| def fingerprint_type(self): | |||||
| return self['fingerprint_type'] | |||||
| @fingerprint_type.setter | |||||
| def fingerprint_type(self, value): | |||||
| self['fingerprint_type'] = value | |||||
| @property | |||||
| def fingerprint(self): | |||||
| return self['fingerprint'] | |||||
| @fingerprint.setter | |||||
| def fingerprint(self, value): | |||||
| self['fingerprint'] = value | |||||
| @property | |||||
| def data(self): | |||||
| return self | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return f'{self.algorithm} {self.fingerprint_type} {self.fingerprint}' | |||||
| def __hash__(self): | |||||
| return hash(self.__repr__()) | |||||
| def _equality_tuple(self): | |||||
| return (self.algorithm, self.fingerprint_type, self.fingerprint) | |||||
| def __repr__(self): | |||||
| return f"'{self.algorithm} {self.fingerprint_type} {self.fingerprint}'" | |||||
| class SshfpRecord(ValuesMixin, Record): | |||||
| _type = 'SSHFP' | |||||
| _value_type = SshfpValue | |||||
| Record.register_type(SshfpRecord) | |||||
| @ -0,0 +1,160 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| from .rr import RrParseError | |||||
| class TlsaValue(EqualityTupleMixin, dict): | |||||
| @classmethod | |||||
| def parse_rdata_text(self, value): | |||||
| try: | |||||
| ( | |||||
| certificate_usage, | |||||
| selector, | |||||
| matching_type, | |||||
| certificate_association_data, | |||||
| ) = value.split(' ') | |||||
| except ValueError: | |||||
| raise RrParseError() | |||||
| try: | |||||
| certificate_usage = int(certificate_usage) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| selector = int(selector) | |||||
| except ValueError: | |||||
| pass | |||||
| try: | |||||
| matching_type = int(matching_type) | |||||
| except ValueError: | |||||
| pass | |||||
| return { | |||||
| 'certificate_usage': certificate_usage, | |||||
| 'selector': selector, | |||||
| 'matching_type': matching_type, | |||||
| 'certificate_association_data': certificate_association_data, | |||||
| } | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| try: | |||||
| certificate_usage = int(value.get('certificate_usage', 0)) | |||||
| if certificate_usage < 0 or certificate_usage > 3: | |||||
| reasons.append( | |||||
| f'invalid certificate_usage ' f'"{certificate_usage}"' | |||||
| ) | |||||
| except ValueError: | |||||
| reasons.append( | |||||
| f'invalid certificate_usage ' | |||||
| f'"{value["certificate_usage"]}"' | |||||
| ) | |||||
| try: | |||||
| selector = int(value.get('selector', 0)) | |||||
| if selector < 0 or selector > 1: | |||||
| reasons.append(f'invalid selector "{selector}"') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid selector "{value["selector"]}"') | |||||
| try: | |||||
| matching_type = int(value.get('matching_type', 0)) | |||||
| if matching_type < 0 or matching_type > 2: | |||||
| reasons.append(f'invalid matching_type "{matching_type}"') | |||||
| except ValueError: | |||||
| reasons.append( | |||||
| f'invalid matching_type ' f'"{value["matching_type"]}"' | |||||
| ) | |||||
| if 'certificate_usage' not in value: | |||||
| reasons.append('missing certificate_usage') | |||||
| if 'selector' not in value: | |||||
| reasons.append('missing selector') | |||||
| if 'matching_type' not in value: | |||||
| reasons.append('missing matching_type') | |||||
| if 'certificate_association_data' not in value: | |||||
| reasons.append('missing certificate_association_data') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'certificate_usage': int(value.get('certificate_usage', 0)), | |||||
| 'selector': int(value.get('selector', 0)), | |||||
| 'matching_type': int(value.get('matching_type', 0)), | |||||
| # force it to a string, in case the hex has only numerical | |||||
| # values and it was converted to an int at some point | |||||
| # TODO: this needed on any others? | |||||
| 'certificate_association_data': str( | |||||
| value['certificate_association_data'] | |||||
| ), | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def certificate_usage(self): | |||||
| return self['certificate_usage'] | |||||
| @certificate_usage.setter | |||||
| def certificate_usage(self, value): | |||||
| self['certificate_usage'] = value | |||||
| @property | |||||
| def selector(self): | |||||
| return self['selector'] | |||||
| @selector.setter | |||||
| def selector(self, value): | |||||
| self['selector'] = value | |||||
| @property | |||||
| def matching_type(self): | |||||
| return self['matching_type'] | |||||
| @matching_type.setter | |||||
| def matching_type(self, value): | |||||
| self['matching_type'] = value | |||||
| @property | |||||
| def certificate_association_data(self): | |||||
| return self['certificate_association_data'] | |||||
| @certificate_association_data.setter | |||||
| def certificate_association_data(self, value): | |||||
| self['certificate_association_data'] = value | |||||
| @property | |||||
| def rdata_text(self): | |||||
| return f'{self.certificate_usage} {self.selector} {self.matching_type} {self.certificate_association_data}' | |||||
| def _equality_tuple(self): | |||||
| return ( | |||||
| self.certificate_usage, | |||||
| self.selector, | |||||
| self.matching_type, | |||||
| self.certificate_association_data, | |||||
| ) | |||||
| def __repr__(self): | |||||
| return ( | |||||
| f"'{self.certificate_usage} {self.selector} '" | |||||
| f"'{self.matching_type} {self.certificate_association_data}'" | |||||
| ) | |||||
| class TlsaRecord(ValuesMixin, Record): | |||||
| _type = 'TLSA' | |||||
| _value_type = TlsaValue | |||||
| Record.register_type(TlsaRecord) | |||||
| @ -0,0 +1,121 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from ..equality import EqualityTupleMixin | |||||
| from .base import Record, ValuesMixin | |||||
| class UrlfwdValue(EqualityTupleMixin, dict): | |||||
| VALID_CODES = (301, 302) | |||||
| VALID_MASKS = (0, 1, 2) | |||||
| VALID_QUERY = (0, 1) | |||||
| @classmethod | |||||
| def validate(cls, data, _type): | |||||
| if not isinstance(data, (list, tuple)): | |||||
| data = (data,) | |||||
| reasons = [] | |||||
| for value in data: | |||||
| try: | |||||
| code = int(value['code']) | |||||
| if code not in cls.VALID_CODES: | |||||
| reasons.append(f'unrecognized return code "{code}"') | |||||
| except KeyError: | |||||
| reasons.append('missing code') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid return code "{value["code"]}"') | |||||
| try: | |||||
| masking = int(value['masking']) | |||||
| if masking not in cls.VALID_MASKS: | |||||
| reasons.append(f'unrecognized masking setting "{masking}"') | |||||
| except KeyError: | |||||
| reasons.append('missing masking') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid masking setting "{value["masking"]}"') | |||||
| try: | |||||
| query = int(value['query']) | |||||
| if query not in cls.VALID_QUERY: | |||||
| reasons.append(f'unrecognized query setting "{query}"') | |||||
| except KeyError: | |||||
| reasons.append('missing query') | |||||
| except ValueError: | |||||
| reasons.append(f'invalid query setting "{value["query"]}"') | |||||
| for k in ('path', 'target'): | |||||
| if k not in value: | |||||
| reasons.append(f'missing {k}') | |||||
| return reasons | |||||
| @classmethod | |||||
| def process(cls, values): | |||||
| return [cls(v) for v in values] | |||||
| def __init__(self, value): | |||||
| super().__init__( | |||||
| { | |||||
| 'path': value['path'], | |||||
| 'target': value['target'], | |||||
| 'code': int(value['code']), | |||||
| 'masking': int(value['masking']), | |||||
| 'query': int(value['query']), | |||||
| } | |||||
| ) | |||||
| @property | |||||
| def path(self): | |||||
| return self['path'] | |||||
| @path.setter | |||||
| def path(self, value): | |||||
| self['path'] = value | |||||
| @property | |||||
| def target(self): | |||||
| return self['target'] | |||||
| @target.setter | |||||
| def target(self, value): | |||||
| self['target'] = value | |||||
| @property | |||||
| def code(self): | |||||
| return self['code'] | |||||
| @code.setter | |||||
| def code(self, value): | |||||
| self['code'] = value | |||||
| @property | |||||
| def masking(self): | |||||
| return self['masking'] | |||||
| @masking.setter | |||||
| def masking(self, value): | |||||
| self['masking'] = value | |||||
| @property | |||||
| def query(self): | |||||
| return self['query'] | |||||
| @query.setter | |||||
| def query(self, value): | |||||
| self['query'] = value | |||||
| def _equality_tuple(self): | |||||
| return (self.path, self.target, self.code, self.masking, self.query) | |||||
| def __hash__(self): | |||||
| return hash( | |||||
| (self.path, self.target, self.code, self.masking, self.query) | |||||
| ) | |||||
| def __repr__(self): | |||||
| return f'"{self.path}" "{self.target}" {self.code} {self.masking} {self.query}' | |||||
| class UrlfwdRecord(ValuesMixin, Record): | |||||
| _type = 'URLFWD' | |||||
| _value_type = UrlfwdValue | |||||
| Record.register_type(UrlfwdRecord) | |||||