"""
The Name module provides a class for DNS names.
The Name class
==============
A name is initialized from a tuple of :py:class:`label.Label`::
from dike import Label, Name, make_name
python_sitename = Name((Label(b'www'),
Label(b'python'),
Label(b'org'),))
We can also create a name from a string using
:py:meth:`Name.fromstr()`, which makes a label for each dot-separated
piece. Each label is converted to
Punycode <https://tools.ietf.org/html/rfc3492>`_::
linux_sitename = Name.fromstr('www.kernel.org')
idn_city = Name.fromstr('Gießen.de')
idn_country = Name.fromstr('België.be')
A bytes value can also be used via
:py:meth:`Name.frombytes()`, which make a label for each dot-separated
piece. It does no further conversion::
bytes_name = Name.frombytes(b'non_unicode.name\\0x80')
Finally, a name can be initialized from a tuple of bytes, which is
useful when creating names from a packet, for example::
tuple_name = Name.fromtuple((b'ftp', b'isc', b'org'))
If you are uncertain of which type you will be using to initialize, or
just want to make life easy, the :py:func:`make_name()` function will
create a name from any supported type::
example_name = make_name('a.good.example')
.. note::
All names are treated as `Fully-Qualified Domain Names (FQDN)
<https://tools.ietf.org/html/rfc8499#page-8>`_, also called
*absolute names*. Names are parsed the same if they end with a
dot, ``"."``, whether they do or not. For example,
``"foo.example."`` is the same as ``"foo.example"`` when used to
initialize a :py:class:`Name` instance.
As with :py:class:`label.Label` instances, :py:class:`Name` instances
are immutable - once initialized the value never changes. This has the
advantage of allowing labels to be used as keys in :py:class:`dict`
and various other places, but does mean that updating a name is not
possible.
Name comparisons use the canonical ordering defined in `RFC 4034
<https://tools.ietf.org/html/rfc4034#section-6.1>`_. This sorts the
names by the right-most label::
assert Name.fromstr('example') < Name.fromstr('Z.a.example')
assert Name.fromstr('Z.a.example') > Name.fromstr('a.example')
assert Name.fromstr('Z.a.eXaMPLe') == Name.fromstr('z.A.ExAmplE')
In general, names can be treated as sequences::
k_root = Name.fromstr('k.root-servers.net')
for label_val in k_root:
print(label_val)
assert 'net.root-servers.k' == str(Name(tuple(reversed(k_root))))
assert 'f.root-servers.net' == 'f' + k_root[1:]
You can use the `in` operator to check for the presence of a given
label in a name. Note that this searches for _labels_, not
_substrings_::
foo = Name.fromstr('foo.example')
assert "example" in "foo.example" # we end with "example"
assert "exam" not in "foo.example" # "exam" is not a label
assert "exam" in str("foo.example") # "exam" is a substring
Similarly the built-in `len()` function will return the number of
labels in a name, rather than the length of the name as a string. You
can get the length of a name either as a `str` or `bytes` value by
explicitly converting first::
assert len(foo) == 2
assert len(str(foo)) == 11
Converting to `bytes` works as expected, except in the case where
there is a dot `.` character in one of the labels. In that case, a
:py:class:`LabelHasDot` exception will be raised. Converting to `str`
also works as expected, except when there is a dot `.` character in
one of the labels (when a :py:class:`LabelHasDot` exception will be
raised) or a non-Unicode byte sequence (when a
:py:class:`UnicodeError` will be raised). To convert to a format in a
safe way, use the :py:meth:`Name.to_presentation()` method::
bar = Name.fromtuple((b'www',b'K\\x80T',b'd.t'))
assert bar.to_presentation() == 'www.K\\\\128T.d\\\\.t.''
To check for parent/child relationships you can use the
:py:meth:`Name.startswith()` or :py:meth:`Name.endswith()` methods, or
slicing::
child = Name.fromstr('child.parent.zone')
parent = child[1:]
assert str(parent) == 'parent.zone'
assert child.endswith(parent)
assert child.endswith('zone')
These methods work very similar to the built-in
:py:meth:`str.startswith()` or :py:meth:`str.endswith()` methods,
including the ability to specify start & end values, as well as
comparing with multiple targets by passing a tuple::
zone_to_check = Name.fromstr('lots-o.info')
assert zone_to_check.endswith(('com', 'net', 'org', 'info'))
A couple of utility functions are also available to check for common
types of names::
host_name = Name.fromstr('valid.hostname)
not_host_name = Name.fromstr('in_valid.hostname')
assert hostname.ishost() == True
assert not_host_name.ishost() == False
assert Name().isroot() == True
assert Name('any-other-name').isroot() == False
"""
from typing import Iterator, Sequence, Tuple, Union
from dike.errors import EmptyLabel, LabelHasDot, NameTooLong
from dike.label import Label, make_label
[docs]class Name:
"""
The :py:class:`Name` constructor requires a single argument, which
is a tuple of :py:class:`Label` objects.
Optionally `canonicalize` can be set to `True`, in which case the
name will be set to the canonical (that is, lowercase) version.
If the name is more than 253 characters long, a
:py:class:`NameTooLong` exception will be raised.
:param val: Value to use when creating the name.
:type val: tuple of Label
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`NameTooLong`
"""
__slots__ = (
'_labels',
'_hash_val',
'_ishost_flag',
'_reversed_bytes',
)
_labels: Tuple[Label, ...]
_hash_val: Union[None, int]
_ishost_flag: bool
# "I put my thing down, flip it and reverse it." - Missy Elliot
#
# If we need to, store our labels in reverse order. This is
# because names sort based on the rightmost labels first. Having
# the labels in reverse order makes DNS name comparisons easy in
# Python.
_reversed_bytes: Union[None, Tuple[bytes, ...]]
def __init__(self, labels: Tuple[Label, ...] = (), *,
canonicalize: bool = False) -> None:
# The _ishost_flag attribute is set when the ishost() method is
# first invoked.
# Make sure our length is not too long.
#
# Note that we are adding up the length byte plus the bytes
# of each label. That can be up to 254 bytes, since we need
# one more 0-byte to terminate the name when converted to wire
# format and fit in our 255-byte name length limit.
name_len = sum((len(bytes(label))+1 for label in labels))
if name_len > 254:
raise NameTooLong(labels)
if canonicalize:
self._labels = tuple(make_label(label, canonicalize=True)
for label in labels)
else:
self._labels = labels
self._hash_val = None
self._reversed_bytes = None
[docs] @classmethod
def fromstr(cls, val: str, *, canonicalize: bool = False) -> 'Name':
"""
Convert the specified value to a Name object, by splitting it
into labels first. It is converted to Punycode as necessary.
Optionally `canonicalize` can be set to `True`, in which case
the name will be set to the canonical (that is, lowercase)
version.
A number of issues will cause an exception to be raised. Note
that whitespace is not treated specially in any way, so if you
want to trim it do so before passing to this method.
:param val: Value to use when creating the name.
:type val: str
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`UnicodeError`
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
"""
# Special-case the root zone.
if val == '.':
return Name()
labels = val.split('.')
# Handle names written as FQDN (with the final dot).
if labels[-1] == '':
labels.pop()
# Handle empty name.
if not labels:
raise EmptyLabel()
initialized_labels = tuple(Label.fromstr(label) for label in labels)
return Name(initialized_labels, canonicalize=canonicalize)
[docs] @classmethod
def frombytes(cls, val: bytes, *, canonicalize: bool = False) -> 'Name':
"""
Convert the specified value to a Name object, by splitting it
into labels first.
Optionally `canonicalize` can be set to `True`, in which case
the name will be set to the canonical (that is, lowercase)
version.
A number of issues will cause an exception to be raised. Note
that whitespace is not treated specially in any way, so if you
want to trim it do so before passing to this method.
:param val: Value to use when creating the name.
:type val: bytes
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
"""
# Special-case the root zone.
if val == b'.':
return Name()
labels = val.split(b'.')
# Handle names written as FQDN (with the final dot).
if labels[-1] == b'':
labels.pop()
# Handle empty name.
if not labels:
raise EmptyLabel()
initialized_labels = tuple(Label(label) for label in labels)
return Name(initialized_labels, canonicalize=canonicalize)
[docs] @classmethod
def fromtuple(cls, val: Tuple[bytes, ...], *,
canonicalize: bool = False) -> 'Name':
"""
Convert the specified value to a Name object, by converting
each element into a :py:class:`Label` first.
Optionally `canonicalize` can be set to `True`, in which case
the name will be set to the canonical (that is, lowercase)
version.
A number of issues will cause an exception to be raised.
:param val: Value to use when creating the name.
:type val: typle of bytes
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
"""
initialized_labels = tuple(Label(label) for label in val)
return Name(initialized_labels, canonicalize=canonicalize)
def _invariant(self) -> None:
assert isinstance(self._labels, tuple)
name_len = 0
for label in self._labels:
assert isinstance(label, Label)
label._invariant() # pylint: disable=protected-access
name_len += len(bytes(label)) + 1
assert 0 <= name_len <= 254
if self._reversed_bytes is not None:
assert isinstance(self._reversed_bytes, tuple)
assert len(self._labels) == len(self._reversed_bytes)
i = len(self._labels)
for label_bytes in self._reversed_bytes:
assert isinstance(label_bytes, bytes)
i -= 1
assert self._labels[i].canonical() == label_bytes
if self._hash_val is not None:
assert isinstance(self._hash_val, int)
assert self._hash_val == hash(self._canonical())
if getattr(self, '_ishost_flag', None) is not None:
assert isinstance(self._ishost_flag, bool)
def _canonical(self) -> Tuple[bytes, ...]:
return tuple(cl.canonical() for cl in self._labels)
def _reversed(self) -> Tuple[bytes, ...]:
if self._reversed_bytes is None:
self._reversed_bytes = tuple(bytes(label.canonical())
for label in reversed(self._labels))
return self._reversed_bytes
[docs] def ishost(self) -> bool:
"""
Test if the name is valid as a host name. The rules for host
names are defined in:
* `RFC 1034 Section 3.5
<https://tools.ietf.org/html/rfc1034#section-3.5>`_
* `RFC 1123 Section 2
<https://tools.ietf.org/html/rfc1123#section-2>`_
:rtype: bool
"""
if getattr(self, "_ishost_flag", None) is None:
ishost_flag = True
if not self._labels:
ishost_flag = False
else:
for name_label in self._labels:
if not name_label.ishost():
ishost_flag = False
break
self._ishost_flag = ishost_flag
return self._ishost_flag
[docs] def isroot(self) -> bool:
"""
Test if the name is the root (that is, has no labels).
:rtype: bool
"""
return not bool(self._labels)
def _stringify(self) -> str:
labels = []
for name_label in self._labels:
label_str = str(name_label)
if '.' in label_str:
raise LabelHasDot(name_label)
labels.append(label_str)
return '.'.join(labels)
def __str__(self) -> str:
if not self._labels:
return '.'
return self._stringify()
def __bytes__(self) -> bytes:
if not self._labels:
return b'.'
labels = []
for name_label in self._labels:
label_bytes = bytes(name_label)
if b'.' in label_bytes:
raise LabelHasDot(name_label)
labels.append(label_bytes)
return b'.'.join(labels)
[docs] def to_presentation(self) -> str:
"""
Return a string of the name converted to the master zone file
presentation format described in
`RFC 1035 <https://tools.ietf.org/html/rfc1035#section-5.1>`_.
:return: presentation format of the name
:rtype: str
"""
if not self._labels:
return "."
presentation = [name_label.to_presentation()
for name_label in self._labels]
return ".".join(presentation) + "."
[docs] def to_wire(self) -> bytes:
"""
Return a DNS wire-format version of the name, without any name
compression.
:return: wire format of the name
:rtype: bytes
"""
# Apparently using a bytearray is the fastest way to
# concatenate bytes:
# https://www.guyrutenberg.com/2020/04/04/fast-bytes-concatenation-in-python/
wire_labels = bytearray()
for label in self._labels:
wire_labels += chr(len(bytes(label))).encode()
wire_labels += bytes(label)
wire_labels += b'\0'
return bytes(wire_labels)
def __repr__(self) -> str:
cls_name = self.__class__.__name__
if not self._labels:
return cls_name + "()"
try:
try:
return cls_name + ".fromstr(" + repr(self._stringify()) + ")"
except UnicodeError:
return cls_name + ".frombytes(" + repr(bytes(self)) + ")"
except LabelHasDot:
labels = ", ".join(repr(label) for label in self._labels)
return cls_name + "([" + labels + "])"
def __len__(self) -> int:
return len(self._labels)
@staticmethod
def _prepare_labels(name_val: Union['Name', Label,
str, bytes]) -> Tuple[Label, ...]:
# pylint: disable=protected-access
if isinstance(name_val, Name):
return name_val._labels
if isinstance(name_val, Label):
return (name_val,)
if isinstance(name_val, str):
return Name.fromstr(name_val)._labels
if isinstance(name_val, bytes):
return Name.frombytes(name_val)._labels
raise TypeError(f"Cannot be converted to a Name instance: {name_val}")
@staticmethod
def _prepare_rev_bytes(name_val: Union['Name', Label,
str, bytes]) -> Tuple[bytes, ...]:
# pylint: disable=protected-access
if isinstance(name_val, Name):
return name_val._reversed()
if isinstance(name_val, Label):
return (name_val.canonical(),)
if isinstance(name_val, str):
return Name.fromstr(name_val)._reversed()
if isinstance(name_val, bytes):
return Name.frombytes(name_val)._reversed()
raise TypeError(f"Cannot be converted to a Name instance: {name_val}")
def __eq__(self, other: object) -> bool:
if not isinstance(other, (Name, Label, str, bytes)):
return NotImplemented
return self._reversed() == Name._prepare_rev_bytes(other)
def __ge__(self, other: Union['Name', Label, str, bytes]) -> bool:
return self._reversed() >= Name._prepare_rev_bytes(other)
def __gt__(self, other: Union['Name', Label, str, bytes]) -> bool:
return self._reversed() > Name._prepare_rev_bytes(other)
def __le__(self, other: Union['Name', Label, str, bytes]) -> bool:
return self._reversed() <= Name._prepare_rev_bytes(other)
def __lt__(self, other: Union['Name', Label, str, bytes]) -> bool:
return self._reversed() < Name._prepare_rev_bytes(other)
def __ne__(self, other: object) -> bool:
if not isinstance(other, (Name, Label, str, bytes)):
return NotImplemented
return self._reversed() != Name._prepare_rev_bytes(other)
def __hash__(self) -> int:
if self._hash_val is None:
self._hash_val = hash(self._canonical())
return self._hash_val
def __contains__(self, other: Union['Name', Label, str, bytes]) -> bool:
# Looking to see if a given name contains another name is
# equivalent to string-searching:
#
# https://en.wikipedia.org/wiki/String-searching_algorithm
#
# We choose a simple set of heuristics and then a naive search
# rather than a more sophisticated algorithm because we expect
# that most DNS names will be relatively short. This is not
# necessarily so, and in the case of IPv6 reverse names this
# is definitely not true, where each name is 34 labels.
if isinstance(other, Label):
return other in self._labels
other_labels = Name._prepare_rev_bytes(other)
my_labels = self._reversed()
if len(other_labels) == 1:
return other_labels[0] in my_labels
if len(other_labels) > len(my_labels):
return False
for ofs in range(len(my_labels) - len(other_labels) + 1):
if my_labels[ofs:ofs+len(other_labels)] == other_labels:
return True
return False
def __add__(self, other: Union['Name', Label, str, bytes]) -> 'Name':
return Name(self._labels + Name._prepare_labels(other))
def __radd__(self, other: Union['Name', Label, str, bytes]) -> 'Name':
return Name(Name._prepare_labels(other) + self._labels)
def __iter__(self) -> Iterator[Label]:
return iter(self._labels)
def __getitem__(self,
slice_obj: Union[int, slice]) -> Union['Name', Label]:
the_slice = self._labels[slice_obj]
if isinstance(the_slice, Label):
return the_slice
return Name(the_slice)
[docs] def endswith(self,
suffix: Union['Name', Label, str, bytes,
Tuple[Union['Name', Label, str, bytes], ...]],
start: int = 0, end: Union[int, None] = None) -> bool:
"""
Return True if the name ends with the specified suffix.
With optional start, test the name beginning at that label.
With optional end, stop comparing the name at that label.
The suffix can be a :py:class:`Name`, :py:class:`Label`,
:py:class:`str`, or :py:class:`bytes`.
The suffix can also be a :py:class:`tuple` of :py:class:`Name`
or things that can be converted to :py:class:`Name`
(:py:class:`str`, :py:class:`bytes`, or :py:class:`label.Label`).
:param suffix: Suffix to check the end of the name for.
:type suffix: str, bytes, :py:class:`Name`, :py:class:`Label`,
or tuple
:param start: Label to start checking at.
:type start: int
:param end: Label to end checking at.
:type end: int
:return: Whether the name ends with the specified prefix.
:rtype: bool
:raises: :py:class:`TypeError` when suffix cannot be compared
"""
# pylint: disable=protected-access
if start > len(self._labels):
return False
if isinstance(suffix, tuple):
suffix_list = suffix
else:
suffix_list = (suffix,)
# As a possible optimization in the future, we can defer the
# creation of label_slice until after the checks for label
# length have been executed.
label_slice = [label.canonical() for label in self._labels[start:end]]
for test_suffix in suffix_list:
try:
suffix_labels = Name._prepare_rev_bytes(test_suffix)
except EmptyLabel:
if isinstance(test_suffix, str) and (test_suffix == ''):
return True
if isinstance(test_suffix, bytes) and (test_suffix == b''):
return True
raise
if len(suffix_labels) > len(label_slice):
continue
if len(suffix_labels) == 0:
return True
end_slice = reversed(label_slice[-len(suffix_labels):])
if tuple(end_slice) == suffix_labels:
return True
return False
[docs] def startswith(self,
prefix: Union['Name', Label, str, bytes,
Tuple[Union['Name', Label, str, bytes], ...]],
start: int = 0, end: Union[int, None] = None) -> bool:
"""
Return True if the name starts with the specified prefix.
With optional start, test the name beginning at that label.
With optional end, stop comparing the name at that label.
The prefix can be a :py:class:`Name`, :py:class:`Label`,
:py:class:`str`, or :py:class:`bytes`.
The prefix can also be a :py:class:`tuple` of :py:class:`Name`
or things that can be converted to :py:class:`Name`
(:py:class:`str`, :py:class:`bytes`, or :py:class:`label.Label`).
:param prefix: Prefix to check the end of the name for.
:type prefix: str, bytes, :py:class:`Name`, :py:class:`Label`,
or tuple
:param start: Label to start checking at.
:type start: int
:param end: Label to end checking at.
:type end: int
:return: Whether the name ends with the specified prefix.
:rtype: bool
:raises: :py:class:`TypeError` when prefix cannot be compared
"""
# pylint: disable=protected-access
if start > len(self._labels):
return False
if isinstance(prefix, tuple):
prefix_list = prefix
else:
prefix_list = (prefix,)
label_slice = [label.canonical() for label in self._labels[start:end]]
for test_prefix in prefix_list:
try:
prefix_labels = Name._prepare_rev_bytes(test_prefix)
except EmptyLabel:
if isinstance(test_prefix, str) and (test_prefix == ''):
return True
if isinstance(test_prefix, bytes) and (test_prefix == b''):
return True
raise
if len(prefix_labels) > len(label_slice):
continue
if len(prefix_labels) == 0:
return True
start_slice = reversed(label_slice[:len(prefix_labels)])
if tuple(start_slice) == prefix_labels:
return True
return False
def make_name(name_val: Union[Name, Label, str, bytes,
Sequence[Union[Label, str, bytes]]], *,
canonicalize: bool = False) -> Name:
"""
Initialize a name from a string, bytes, label, or a sequence
containing these (such as a list or tuple). It can also be
initialized from another name.
Optionally `canonicalize` can be set to `True`, in which case the
name will be set to the canonical (that is, lowercase) version.
:param name_val: Value to create a new name from.
:type prefix: str, bytes, :py:class:`Name`, :py:class:`Label`,
or a sequence of str, bytes, or :py:class:`Label`
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:return: an initialized name
:rtype: Name
:raises: :py:class:`UnicodeError`
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
:raises: :py:class:`TypeError`
"""
# pylint: disable=protected-access
if isinstance(name_val, Name):
if canonicalize:
result = Name(name_val._labels, canonicalize=True)
else:
result = name_val
elif isinstance(name_val, Label):
result = Name((name_val,), canonicalize=canonicalize)
elif isinstance(name_val, str):
result = Name.fromstr(name_val, canonicalize=canonicalize)
elif isinstance(name_val, bytes):
result = Name.frombytes(name_val, canonicalize=canonicalize)
elif isinstance(name_val, Sequence):
labels = []
for val in name_val:
if not isinstance(val, (Label, str, bytes)):
allowed = "a string, bytes, or Label"
disallowed = (type(name_val).__module__ + '.' +
type(name_val).__name__)
raise TypeError("make_name() argument in a tuple must be "
f"{allowed}, not '{disallowed}'")
labels.append(make_label(val))
result = Name(tuple(labels), canonicalize=canonicalize)
else:
allowed = "a string, bytes, Label, or Name"
disallowed = type(name_val).__module__ + '.' + type(name_val).__name__
raise TypeError(f"make_name() argument must be {allowed}, "
f"not '{disallowed}'")
return result