Source code for dyndns.names

"""Deal with different kind of names (FQDNs (Fully Qualified Domain Names),
record and zone names)

``record_name`` + ``zone_name`` = ``fqdn``

"""

# standard imports
import binascii
import re

# third party imports
import dns.name
import dns.tsigkeyring
import dns.tsig
from dyndns.exceptions import NamesError


[docs]def validate_hostname(hostname): if hostname[-1] == ".": # strip exactly one dot from the right, if present hostname = hostname[:-1] if len(hostname) > 253: raise NamesError( 'The hostname "{}..." is longer than 253 characters.' .format(hostname[:10]) ) labels = hostname.split(".") tld = labels[-1] if re.match(r"[0-9]+$", tld): raise NamesError( 'The TLD "{}" of the hostname "{}" must be not all-numeric.' .format(tld, hostname) ) allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE) for label in labels: if not allowed.match(label): raise NamesError( 'The label "{}" of the hostname "{}" is invalid.' .format(label, hostname) ) return str(dns.name.from_text(hostname))
[docs]def validate_tsig_key(tsig_key): if not tsig_key: raise NamesError('Invalid tsig key: "{}".'.format(tsig_key)) try: dns.tsigkeyring.from_text({'tmp.org.': tsig_key}) return tsig_key except binascii.Error: raise NamesError('Invalid tsig key: "{}".'.format(tsig_key))
[docs]class Zone: def __init__(self, zone_name, tsig_key): self.zone_name = validate_hostname(zone_name) self.tsig_key = validate_tsig_key(tsig_key)
[docs] def split_fqdn(self, fqdn): """Split hostname into record_name and zone_name for example: www.example.com -> www. example.com. """ fqdn = validate_hostname(fqdn) record_name = fqdn.replace(self.zone_name, '') if record_name and len(record_name) < len(fqdn): return (record_name, self.zone_name) raise NamesError('FQDN "{}" is not splitable by zone "{}".')
[docs] def build_fqdn(self, record_name): record_name = validate_hostname(record_name) return record_name + self.zone_name
[docs]class Zones: def __init__(self, zones_config): self.zones = {} for zone_config in zones_config: zone = Zone( zone_name=zone_config['name'], tsig_key=zone_config['tsig_key'] ) self.zones[zone.zone_name] = zone
[docs] def get_zone_by_name(self, zone_name): zone_name = validate_hostname(zone_name) if zone_name in self.zones: return self.zones[validate_hostname(zone_name)] raise NamesError('Unkown zone "{}".'.format(zone_name))
[docs] def split_fqdn(self, fqdn): """Split hostname into record_name and zone_name for example: www.example.com -> www. example.com. """ fqdn = validate_hostname(fqdn) # To handle subzones (example.com and dyndns.example.com) results = {} for _, zone in self.zones.items(): record_name = fqdn.replace(zone.zone_name, '') if record_name and len(record_name) < len(fqdn): results[len(record_name)] = (record_name, zone.zone_name) for key in sorted(results): return results[key] return False
[docs]class Names: def __init__(self, zones, fqdn=None, zone_name=None, record_name=None): self.fqdn = None """The Fully Qualified Domain Name (e. g. ``www.example.com.``)""" self.zone_name = None """The zone name (e. g. ``example.com.``)""" self.record_name = None """The name resource record (e. g. ``www.``)""" if fqdn and zone_name and record_name: raise NamesError('Specify "fqdn" or "zone_name" and ' '"record_name".') self._zones = zones if fqdn: self.fqdn = validate_hostname(fqdn) split = self._zones.split_fqdn(fqdn) if split: self.record_name = split[0] self.zone_name = split[1] if not fqdn and zone_name and record_name: self.record_name = validate_hostname(record_name) self.zone_name = validate_hostname(zone_name) zone = self._zones.get_zone_by_name(self.zone_name) self.fqdn = zone.build_fqdn(self.record_name) if not self.record_name: raise NamesError('Value "record_name" is required.') if not self.zone_name: raise NamesError('Value "zone_name" is required.') self._zone = self._zones.get_zone_by_name(self.zone_name) self.tsig_key = self._zone.tsig_key """The twig key (e. g. ``tPyvZA==``)"""