You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
import re
|
|
|
|
from fqdn._compat import cached_property
|
|
|
|
|
|
class FQDN:
|
|
"""
|
|
From https://tools.ietf.org/html/rfc1035#page-9, RFC 1035 3.1. Name space
|
|
definitions:
|
|
|
|
Domain names in messages are expressed in terms of a sequence of
|
|
labels. Each label is represented as a one octet length field followed
|
|
by that number of octets. Since every domain name ends with the null
|
|
label of the root, a domain name is terminated by a length byte of
|
|
zero. The high order two bits of every length octet must be zero, and
|
|
the remaining six bits of the length field limit the label to 63 octets
|
|
or less.
|
|
|
|
To simplify implementations, the total length of a domain name (i.e.,
|
|
label octets and label length octets) is restricted to 255 octets or
|
|
less.
|
|
|
|
|
|
Therefore the max length of a domain name is actually 253 ASCII bytes
|
|
without the trailing null byte or the leading length byte, and the max
|
|
length of a label is 63 bytes without the leading length byte.
|
|
"""
|
|
|
|
PREFERRED_NAME_SYNTAX_REGEXSTR = (
|
|
r"^((?![-])[-A-Z\d]{1,63}(?<!-)[.])*(?!-)[-A-Z\d]{1,63}(?<!-)[.]?$"
|
|
)
|
|
ALLOW_UNDERSCORES_REGEXSTR = (
|
|
r"^((?![-])[-_A-Z\d]{1,63}(?<!-)[.])*(?!-)[-_A-Z\d]{1,63}(?<!-)[.]?$"
|
|
)
|
|
|
|
def __init__(self, fqdn, *nothing, **kwargs):
|
|
if nothing:
|
|
raise ValueError("got extra positional parameter, try kwargs")
|
|
unknown_kwargs = set(kwargs.keys()) - {"allow_underscores", "min_labels"}
|
|
if unknown_kwargs:
|
|
raise ValueError("got extra kwargs: {}".format(unknown_kwargs))
|
|
|
|
if not (fqdn and isinstance(fqdn, str)):
|
|
raise ValueError("fqdn must be str")
|
|
self._fqdn = fqdn.lower()
|
|
self._allow_underscores = kwargs.get("allow_underscores", False)
|
|
self._min_labels = kwargs.get("min_labels", 2)
|
|
|
|
def __str__(self):
|
|
"""
|
|
The FQDN as a string in absolute form
|
|
"""
|
|
return self.absolute
|
|
|
|
@property
|
|
def _regex(self):
|
|
regexstr = (
|
|
FQDN.PREFERRED_NAME_SYNTAX_REGEXSTR
|
|
if not self._allow_underscores
|
|
else FQDN.ALLOW_UNDERSCORES_REGEXSTR
|
|
)
|
|
return re.compile(regexstr, re.IGNORECASE)
|
|
|
|
@cached_property
|
|
def is_valid(self):
|
|
"""
|
|
True for a validated fully-qualified domain nam (FQDN), in full
|
|
compliance with RFC 1035, and the "preferred form" specified in RFC
|
|
3686 s. 2, whether relative or absolute.
|
|
|
|
https://tools.ietf.org/html/rfc3696#section-2
|
|
https://tools.ietf.org/html/rfc1035
|
|
|
|
If and only if the FQDN ends with a dot (in place of the RFC1035
|
|
trailing null byte), it may have a total length of 254 bytes, still it
|
|
must be less than 253 bytes.
|
|
"""
|
|
length = len(self._fqdn)
|
|
if self._fqdn.endswith("."):
|
|
length -= 1
|
|
if length > 253:
|
|
return False
|
|
regex_pass = self._regex.match(self._fqdn)
|
|
if not regex_pass:
|
|
return False
|
|
|
|
return self.labels_count >= self._min_labels
|
|
|
|
@property
|
|
def labels_count(self):
|
|
has_terminal_dot = self._fqdn[-1] == "."
|
|
count = self._fqdn.count(".") + (0 if has_terminal_dot else 1)
|
|
return count
|
|
|
|
@cached_property
|
|
def is_valid_absolute(self):
|
|
"""
|
|
True for a fully-qualified domain name (FQDN) that is RFC
|
|
preferred-form compliant and ends with a `.`.
|
|
|
|
With relative FQDNS in DNS lookups, the current hosts domain name or
|
|
search domains may be appended.
|
|
"""
|
|
return self._fqdn.endswith(".") and self.is_valid
|
|
|
|
@cached_property
|
|
def is_valid_relative(self):
|
|
"""
|
|
True for a validated fully-qualified domain name that compiles with the
|
|
RFC preferred-form and does not ends with a `.`.
|
|
"""
|
|
return not self._fqdn.endswith(".") and self.is_valid
|
|
|
|
@cached_property
|
|
def absolute(self):
|
|
"""
|
|
The FQDN as a string in absolute form
|
|
"""
|
|
if not self.is_valid:
|
|
raise ValueError("invalid FQDN `{0}`".format(self._fqdn))
|
|
|
|
if self.is_valid_absolute:
|
|
return self._fqdn
|
|
|
|
return "{0}.".format(self._fqdn)
|
|
|
|
@cached_property
|
|
def relative(self):
|
|
"""
|
|
The FQDN as a string in relative form
|
|
"""
|
|
if not self.is_valid:
|
|
raise ValueError("invalid FQDN `{0}`".format(self._fqdn))
|
|
|
|
if self.is_valid_absolute:
|
|
return self._fqdn[:-1]
|
|
|
|
return self._fqdn
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, FQDN):
|
|
return self.absolute == other.absolute
|
|
|
|
def __hash__(self):
|
|
return hash(self.absolute) + hash("fqdn")
|