123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284 |
- # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
- # Copyright (C) 2001-2017 Nominum, Inc.
- #
- # Permission to use, copy, modify, and distribute this software and its
- # documentation for any purpose with or without fee is hereby granted,
- # provided that the above copyright notice and this permission notice
- # appear in all copies.
- #
- # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
- # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
- # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
- # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- """DNS Names.
- """
- import copy
- import encodings.idna # type: ignore
- import functools
- import struct
- from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union
- import dns._features
- import dns.enum
- import dns.exception
- import dns.immutable
- import dns.wire
- if dns._features.have("idna"):
- import idna # type: ignore
- have_idna_2008 = True
- else: # pragma: no cover
- have_idna_2008 = False
- CompressType = Dict["Name", int]
- class NameRelation(dns.enum.IntEnum):
- """Name relation result from fullcompare()."""
- # This is an IntEnum for backwards compatibility in case anyone
- # has hardwired the constants.
- #: The compared names have no relationship to each other.
- NONE = 0
- #: the first name is a superdomain of the second.
- SUPERDOMAIN = 1
- #: The first name is a subdomain of the second.
- SUBDOMAIN = 2
- #: The compared names are equal.
- EQUAL = 3
- #: The compared names have a common ancestor.
- COMMONANCESTOR = 4
- @classmethod
- def _maximum(cls):
- return cls.COMMONANCESTOR # pragma: no cover
- @classmethod
- def _short_name(cls):
- return cls.__name__ # pragma: no cover
- # Backwards compatibility
- NAMERELN_NONE = NameRelation.NONE
- NAMERELN_SUPERDOMAIN = NameRelation.SUPERDOMAIN
- NAMERELN_SUBDOMAIN = NameRelation.SUBDOMAIN
- NAMERELN_EQUAL = NameRelation.EQUAL
- NAMERELN_COMMONANCESTOR = NameRelation.COMMONANCESTOR
- class EmptyLabel(dns.exception.SyntaxError):
- """A DNS label is empty."""
- class BadEscape(dns.exception.SyntaxError):
- """An escaped code in a text format of DNS name is invalid."""
- class BadPointer(dns.exception.FormError):
- """A DNS compression pointer points forward instead of backward."""
- class BadLabelType(dns.exception.FormError):
- """The label type in DNS name wire format is unknown."""
- class NeedAbsoluteNameOrOrigin(dns.exception.DNSException):
- """An attempt was made to convert a non-absolute name to
- wire when there was also a non-absolute (or missing) origin."""
- class NameTooLong(dns.exception.FormError):
- """A DNS name is > 255 octets long."""
- class LabelTooLong(dns.exception.SyntaxError):
- """A DNS label is > 63 octets long."""
- class AbsoluteConcatenation(dns.exception.DNSException):
- """An attempt was made to append anything other than the
- empty name to an absolute DNS name."""
- class NoParent(dns.exception.DNSException):
- """An attempt was made to get the parent of the root name
- or the empty name."""
- class NoIDNA2008(dns.exception.DNSException):
- """IDNA 2008 processing was requested but the idna module is not
- available."""
- class IDNAException(dns.exception.DNSException):
- """IDNA processing raised an exception."""
- supp_kwargs = {"idna_exception"}
- fmt = "IDNA processing exception: {idna_exception}"
- # We do this as otherwise mypy complains about unexpected keyword argument
- # idna_exception
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- class NeedSubdomainOfOrigin(dns.exception.DNSException):
- """An absolute name was provided that is not a subdomain of the specified origin."""
- _escaped = b'"().;\\@$'
- _escaped_text = '"().;\\@$'
- def _escapify(label: Union[bytes, str]) -> str:
- """Escape the characters in label which need it.
- @returns: the escaped string
- @rtype: string"""
- if isinstance(label, bytes):
- # Ordinary DNS label mode. Escape special characters and values
- # < 0x20 or > 0x7f.
- text = ""
- for c in label:
- if c in _escaped:
- text += "\\" + chr(c)
- elif c > 0x20 and c < 0x7F:
- text += chr(c)
- else:
- text += "\\%03d" % c
- return text
- # Unicode label mode. Escape only special characters and values < 0x20
- text = ""
- for uc in label:
- if uc in _escaped_text:
- text += "\\" + uc
- elif uc <= "\x20":
- text += "\\%03d" % ord(uc)
- else:
- text += uc
- return text
- class IDNACodec:
- """Abstract base class for IDNA encoder/decoders."""
- def __init__(self):
- pass
- def is_idna(self, label: bytes) -> bool:
- return label.lower().startswith(b"xn--")
- def encode(self, label: str) -> bytes:
- raise NotImplementedError # pragma: no cover
- def decode(self, label: bytes) -> str:
- # We do not apply any IDNA policy on decode.
- if self.is_idna(label):
- try:
- slabel = label[4:].decode("punycode")
- return _escapify(slabel)
- except Exception as e:
- raise IDNAException(idna_exception=e)
- else:
- return _escapify(label)
- class IDNA2003Codec(IDNACodec):
- """IDNA 2003 encoder/decoder."""
- def __init__(self, strict_decode: bool = False):
- """Initialize the IDNA 2003 encoder/decoder.
- *strict_decode* is a ``bool``. If `True`, then IDNA2003 checking
- is done when decoding. This can cause failures if the name
- was encoded with IDNA2008. The default is `False`.
- """
- super().__init__()
- self.strict_decode = strict_decode
- def encode(self, label: str) -> bytes:
- """Encode *label*."""
- if label == "":
- return b""
- try:
- return encodings.idna.ToASCII(label)
- except UnicodeError:
- raise LabelTooLong
- def decode(self, label: bytes) -> str:
- """Decode *label*."""
- if not self.strict_decode:
- return super().decode(label)
- if label == b"":
- return ""
- try:
- return _escapify(encodings.idna.ToUnicode(label))
- except Exception as e:
- raise IDNAException(idna_exception=e)
- class IDNA2008Codec(IDNACodec):
- """IDNA 2008 encoder/decoder."""
- def __init__(
- self,
- uts_46: bool = False,
- transitional: bool = False,
- allow_pure_ascii: bool = False,
- strict_decode: bool = False,
- ):
- """Initialize the IDNA 2008 encoder/decoder.
- *uts_46* is a ``bool``. If True, apply Unicode IDNA
- compatibility processing as described in Unicode Technical
- Standard #46 (https://unicode.org/reports/tr46/).
- If False, do not apply the mapping. The default is False.
- *transitional* is a ``bool``: If True, use the
- "transitional" mode described in Unicode Technical Standard
- #46. The default is False.
- *allow_pure_ascii* is a ``bool``. If True, then a label which
- consists of only ASCII characters is allowed. This is less
- strict than regular IDNA 2008, but is also necessary for mixed
- names, e.g. a name with starting with "_sip._tcp." and ending
- in an IDN suffix which would otherwise be disallowed. The
- default is False.
- *strict_decode* is a ``bool``: If True, then IDNA2008 checking
- is done when decoding. This can cause failures if the name
- was encoded with IDNA2003. The default is False.
- """
- super().__init__()
- self.uts_46 = uts_46
- self.transitional = transitional
- self.allow_pure_ascii = allow_pure_ascii
- self.strict_decode = strict_decode
- def encode(self, label: str) -> bytes:
- if label == "":
- return b""
- if self.allow_pure_ascii and is_all_ascii(label):
- encoded = label.encode("ascii")
- if len(encoded) > 63:
- raise LabelTooLong
- return encoded
- if not have_idna_2008:
- raise NoIDNA2008
- try:
- if self.uts_46:
- # pylint: disable=possibly-used-before-assignment
- label = idna.uts46_remap(label, False, self.transitional)
- return idna.alabel(label)
- except idna.IDNAError as e:
- if e.args[0] == "Label too long":
- raise LabelTooLong
- else:
- raise IDNAException(idna_exception=e)
- def decode(self, label: bytes) -> str:
- if not self.strict_decode:
- return super().decode(label)
- if label == b"":
- return ""
- if not have_idna_2008:
- raise NoIDNA2008
- try:
- ulabel = idna.ulabel(label)
- if self.uts_46:
- ulabel = idna.uts46_remap(ulabel, False, self.transitional)
- return _escapify(ulabel)
- except (idna.IDNAError, UnicodeError) as e:
- raise IDNAException(idna_exception=e)
- IDNA_2003_Practical = IDNA2003Codec(False)
- IDNA_2003_Strict = IDNA2003Codec(True)
- IDNA_2003 = IDNA_2003_Practical
- IDNA_2008_Practical = IDNA2008Codec(True, False, True, False)
- IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False)
- IDNA_2008_Strict = IDNA2008Codec(False, False, False, True)
- IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False)
- IDNA_2008 = IDNA_2008_Practical
- def _validate_labels(labels: Tuple[bytes, ...]) -> None:
- """Check for empty labels in the middle of a label sequence,
- labels that are too long, and for too many labels.
- Raises ``dns.name.NameTooLong`` if the name as a whole is too long.
- Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root
- label) and appears in a position other than the end of the label
- sequence
- """
- l = len(labels)
- total = 0
- i = -1
- j = 0
- for label in labels:
- ll = len(label)
- total += ll + 1
- if ll > 63:
- raise LabelTooLong
- if i < 0 and label == b"":
- i = j
- j += 1
- if total > 255:
- raise NameTooLong
- if i >= 0 and i != l - 1:
- raise EmptyLabel
- def _maybe_convert_to_binary(label: Union[bytes, str]) -> bytes:
- """If label is ``str``, convert it to ``bytes``. If it is already
- ``bytes`` just return it.
- """
- if isinstance(label, bytes):
- return label
- if isinstance(label, str):
- return label.encode()
- raise ValueError # pragma: no cover
- @dns.immutable.immutable
- class Name:
- """A DNS name.
- The dns.name.Name class represents a DNS name as a tuple of
- labels. Each label is a ``bytes`` in DNS wire format. Instances
- of the class are immutable.
- """
- __slots__ = ["labels"]
- def __init__(self, labels: Iterable[Union[bytes, str]]):
- """*labels* is any iterable whose values are ``str`` or ``bytes``."""
- blabels = [_maybe_convert_to_binary(x) for x in labels]
- self.labels = tuple(blabels)
- _validate_labels(self.labels)
- def __copy__(self):
- return Name(self.labels)
- def __deepcopy__(self, memo):
- return Name(copy.deepcopy(self.labels, memo))
- def __getstate__(self):
- # Names can be pickled
- return {"labels": self.labels}
- def __setstate__(self, state):
- super().__setattr__("labels", state["labels"])
- _validate_labels(self.labels)
- def is_absolute(self) -> bool:
- """Is the most significant label of this name the root label?
- Returns a ``bool``.
- """
- return len(self.labels) > 0 and self.labels[-1] == b""
- def is_wild(self) -> bool:
- """Is this name wild? (I.e. Is the least significant label '*'?)
- Returns a ``bool``.
- """
- return len(self.labels) > 0 and self.labels[0] == b"*"
- def __hash__(self) -> int:
- """Return a case-insensitive hash of the name.
- Returns an ``int``.
- """
- h = 0
- for label in self.labels:
- for c in label.lower():
- h += (h << 3) + c
- return h
- def fullcompare(self, other: "Name") -> Tuple[NameRelation, int, int]:
- """Compare two names, returning a 3-tuple
- ``(relation, order, nlabels)``.
- *relation* describes the relation ship between the names,
- and is one of: ``dns.name.NameRelation.NONE``,
- ``dns.name.NameRelation.SUPERDOMAIN``, ``dns.name.NameRelation.SUBDOMAIN``,
- ``dns.name.NameRelation.EQUAL``, or ``dns.name.NameRelation.COMMONANCESTOR``.
- *order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and ==
- 0 if *self* == *other*. A relative name is always less than an
- absolute name. If both names have the same relativity, then
- the DNSSEC order relation is used to order them.
- *nlabels* is the number of significant labels that the two names
- have in common.
- Here are some examples. Names ending in "." are absolute names,
- those not ending in "." are relative names.
- ============= ============= =========== ===== =======
- self other relation order nlabels
- ============= ============= =========== ===== =======
- www.example. www.example. equal 0 3
- www.example. example. subdomain > 0 2
- example. www.example. superdomain < 0 2
- example1.com. example2.com. common anc. < 0 2
- example1 example2. none < 0 0
- example1. example2 none > 0 0
- ============= ============= =========== ===== =======
- """
- sabs = self.is_absolute()
- oabs = other.is_absolute()
- if sabs != oabs:
- if sabs:
- return (NameRelation.NONE, 1, 0)
- else:
- return (NameRelation.NONE, -1, 0)
- l1 = len(self.labels)
- l2 = len(other.labels)
- ldiff = l1 - l2
- if ldiff < 0:
- l = l1
- else:
- l = l2
- order = 0
- nlabels = 0
- namereln = NameRelation.NONE
- while l > 0:
- l -= 1
- l1 -= 1
- l2 -= 1
- label1 = self.labels[l1].lower()
- label2 = other.labels[l2].lower()
- if label1 < label2:
- order = -1
- if nlabels > 0:
- namereln = NameRelation.COMMONANCESTOR
- return (namereln, order, nlabels)
- elif label1 > label2:
- order = 1
- if nlabels > 0:
- namereln = NameRelation.COMMONANCESTOR
- return (namereln, order, nlabels)
- nlabels += 1
- order = ldiff
- if ldiff < 0:
- namereln = NameRelation.SUPERDOMAIN
- elif ldiff > 0:
- namereln = NameRelation.SUBDOMAIN
- else:
- namereln = NameRelation.EQUAL
- return (namereln, order, nlabels)
- def is_subdomain(self, other: "Name") -> bool:
- """Is self a subdomain of other?
- Note that the notion of subdomain includes equality, e.g.
- "dnspython.org" is a subdomain of itself.
- Returns a ``bool``.
- """
- (nr, _, _) = self.fullcompare(other)
- if nr == NameRelation.SUBDOMAIN or nr == NameRelation.EQUAL:
- return True
- return False
- def is_superdomain(self, other: "Name") -> bool:
- """Is self a superdomain of other?
- Note that the notion of superdomain includes equality, e.g.
- "dnspython.org" is a superdomain of itself.
- Returns a ``bool``.
- """
- (nr, _, _) = self.fullcompare(other)
- if nr == NameRelation.SUPERDOMAIN or nr == NameRelation.EQUAL:
- return True
- return False
- def canonicalize(self) -> "Name":
- """Return a name which is equal to the current name, but is in
- DNSSEC canonical form.
- """
- return Name([x.lower() for x in self.labels])
- def __eq__(self, other):
- if isinstance(other, Name):
- return self.fullcompare(other)[1] == 0
- else:
- return False
- def __ne__(self, other):
- if isinstance(other, Name):
- return self.fullcompare(other)[1] != 0
- else:
- return True
- def __lt__(self, other):
- if isinstance(other, Name):
- return self.fullcompare(other)[1] < 0
- else:
- return NotImplemented
- def __le__(self, other):
- if isinstance(other, Name):
- return self.fullcompare(other)[1] <= 0
- else:
- return NotImplemented
- def __ge__(self, other):
- if isinstance(other, Name):
- return self.fullcompare(other)[1] >= 0
- else:
- return NotImplemented
- def __gt__(self, other):
- if isinstance(other, Name):
- return self.fullcompare(other)[1] > 0
- else:
- return NotImplemented
- def __repr__(self):
- return "<DNS name " + self.__str__() + ">"
- def __str__(self):
- return self.to_text(False)
- def to_text(self, omit_final_dot: bool = False) -> str:
- """Convert name to DNS text format.
- *omit_final_dot* is a ``bool``. If True, don't emit the final
- dot (denoting the root label) for absolute names. The default
- is False.
- Returns a ``str``.
- """
- if len(self.labels) == 0:
- return "@"
- if len(self.labels) == 1 and self.labels[0] == b"":
- return "."
- if omit_final_dot and self.is_absolute():
- l = self.labels[:-1]
- else:
- l = self.labels
- s = ".".join(map(_escapify, l))
- return s
- def to_unicode(
- self, omit_final_dot: bool = False, idna_codec: Optional[IDNACodec] = None
- ) -> str:
- """Convert name to Unicode text format.
- IDN ACE labels are converted to Unicode.
- *omit_final_dot* is a ``bool``. If True, don't emit the final
- dot (denoting the root label) for absolute names. The default
- is False.
- *idna_codec* specifies the IDNA encoder/decoder. If None, the
- dns.name.IDNA_2003_Practical encoder/decoder is used.
- The IDNA_2003_Practical decoder does
- not impose any policy, it just decodes punycode, so if you
- don't want checking for compliance, you can use this decoder
- for IDNA2008 as well.
- Returns a ``str``.
- """
- if len(self.labels) == 0:
- return "@"
- if len(self.labels) == 1 and self.labels[0] == b"":
- return "."
- if omit_final_dot and self.is_absolute():
- l = self.labels[:-1]
- else:
- l = self.labels
- if idna_codec is None:
- idna_codec = IDNA_2003_Practical
- return ".".join([idna_codec.decode(x) for x in l])
- def to_digestable(self, origin: Optional["Name"] = None) -> bytes:
- """Convert name to a format suitable for digesting in hashes.
- The name is canonicalized and converted to uncompressed wire
- format. All names in wire format are absolute. If the name
- is a relative name, then an origin must be supplied.
- *origin* is a ``dns.name.Name`` or ``None``. If the name is
- relative and origin is not ``None``, then origin will be appended
- to the name.
- Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
- relative and no origin was provided.
- Returns a ``bytes``.
- """
- digest = self.to_wire(origin=origin, canonicalize=True)
- assert digest is not None
- return digest
- def to_wire(
- self,
- file: Optional[Any] = None,
- compress: Optional[CompressType] = None,
- origin: Optional["Name"] = None,
- canonicalize: bool = False,
- ) -> Optional[bytes]:
- """Convert name to wire format, possibly compressing it.
- *file* is the file where the name is emitted (typically an
- io.BytesIO file). If ``None`` (the default), a ``bytes``
- containing the wire name will be returned.
- *compress*, a ``dict``, is the compression table to use. If
- ``None`` (the default), names will not be compressed. Note that
- the compression code assumes that compression offset 0 is the
- start of *file*, and thus compression will not be correct
- if this is not the case.
- *origin* is a ``dns.name.Name`` or ``None``. If the name is
- relative and origin is not ``None``, then *origin* will be appended
- to it.
- *canonicalize*, a ``bool``, indicates whether the name should
- be canonicalized; that is, converted to a format suitable for
- digesting in hashes.
- Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
- relative and no origin was provided.
- Returns a ``bytes`` or ``None``.
- """
- if file is None:
- out = bytearray()
- for label in self.labels:
- out.append(len(label))
- if canonicalize:
- out += label.lower()
- else:
- out += label
- if not self.is_absolute():
- if origin is None or not origin.is_absolute():
- raise NeedAbsoluteNameOrOrigin
- for label in origin.labels:
- out.append(len(label))
- if canonicalize:
- out += label.lower()
- else:
- out += label
- return bytes(out)
- labels: Iterable[bytes]
- if not self.is_absolute():
- if origin is None or not origin.is_absolute():
- raise NeedAbsoluteNameOrOrigin
- labels = list(self.labels)
- labels.extend(list(origin.labels))
- else:
- labels = self.labels
- i = 0
- for label in labels:
- n = Name(labels[i:])
- i += 1
- if compress is not None:
- pos = compress.get(n)
- else:
- pos = None
- if pos is not None:
- value = 0xC000 + pos
- s = struct.pack("!H", value)
- file.write(s)
- break
- else:
- if compress is not None and len(n) > 1:
- pos = file.tell()
- if pos <= 0x3FFF:
- compress[n] = pos
- l = len(label)
- file.write(struct.pack("!B", l))
- if l > 0:
- if canonicalize:
- file.write(label.lower())
- else:
- file.write(label)
- return None
- def __len__(self) -> int:
- """The length of the name (in labels).
- Returns an ``int``.
- """
- return len(self.labels)
- def __getitem__(self, index):
- return self.labels[index]
- def __add__(self, other):
- return self.concatenate(other)
- def __sub__(self, other):
- return self.relativize(other)
- def split(self, depth: int) -> Tuple["Name", "Name"]:
- """Split a name into a prefix and suffix names at the specified depth.
- *depth* is an ``int`` specifying the number of labels in the suffix
- Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the
- name.
- Returns the tuple ``(prefix, suffix)``.
- """
- l = len(self.labels)
- if depth == 0:
- return (self, dns.name.empty)
- elif depth == l:
- return (dns.name.empty, self)
- elif depth < 0 or depth > l:
- raise ValueError("depth must be >= 0 and <= the length of the name")
- return (Name(self[:-depth]), Name(self[-depth:]))
- def concatenate(self, other: "Name") -> "Name":
- """Return a new name which is the concatenation of self and other.
- Raises ``dns.name.AbsoluteConcatenation`` if the name is
- absolute and *other* is not the empty name.
- Returns a ``dns.name.Name``.
- """
- if self.is_absolute() and len(other) > 0:
- raise AbsoluteConcatenation
- labels = list(self.labels)
- labels.extend(list(other.labels))
- return Name(labels)
- def relativize(self, origin: "Name") -> "Name":
- """If the name is a subdomain of *origin*, return a new name which is
- the name relative to origin. Otherwise return the name.
- For example, relativizing ``www.dnspython.org.`` to origin
- ``dnspython.org.`` returns the name ``www``. Relativizing ``example.``
- to origin ``dnspython.org.`` returns ``example.``.
- Returns a ``dns.name.Name``.
- """
- if origin is not None and self.is_subdomain(origin):
- return Name(self[: -len(origin)])
- else:
- return self
- def derelativize(self, origin: "Name") -> "Name":
- """If the name is a relative name, return a new name which is the
- concatenation of the name and origin. Otherwise return the name.
- For example, derelativizing ``www`` to origin ``dnspython.org.``
- returns the name ``www.dnspython.org.``. Derelativizing ``example.``
- to origin ``dnspython.org.`` returns ``example.``.
- Returns a ``dns.name.Name``.
- """
- if not self.is_absolute():
- return self.concatenate(origin)
- else:
- return self
- def choose_relativity(
- self, origin: Optional["Name"] = None, relativize: bool = True
- ) -> "Name":
- """Return a name with the relativity desired by the caller.
- If *origin* is ``None``, then the name is returned.
- Otherwise, if *relativize* is ``True`` the name is
- relativized, and if *relativize* is ``False`` the name is
- derelativized.
- Returns a ``dns.name.Name``.
- """
- if origin:
- if relativize:
- return self.relativize(origin)
- else:
- return self.derelativize(origin)
- else:
- return self
- def parent(self) -> "Name":
- """Return the parent of the name.
- For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``.
- Raises ``dns.name.NoParent`` if the name is either the root name or the
- empty name, and thus has no parent.
- Returns a ``dns.name.Name``.
- """
- if self == root or self == empty:
- raise NoParent
- return Name(self.labels[1:])
- def predecessor(self, origin: "Name", prefix_ok: bool = True) -> "Name":
- """Return the maximal predecessor of *name* in the DNSSEC ordering in the zone
- whose origin is *origin*, or return the longest name under *origin* if the
- name is origin (i.e. wrap around to the longest name, which may still be
- *origin* due to length considerations.
- The relativity of the name is preserved, so if this name is relative
- then the method will return a relative name, and likewise if this name
- is absolute then the predecessor will be absolute.
- *prefix_ok* indicates if prefixing labels is allowed, and
- defaults to ``True``. Normally it is good to allow this, but if computing
- a maximal predecessor at a zone cut point then ``False`` must be specified.
- """
- return _handle_relativity_and_call(
- _absolute_predecessor, self, origin, prefix_ok
- )
- def successor(self, origin: "Name", prefix_ok: bool = True) -> "Name":
- """Return the minimal successor of *name* in the DNSSEC ordering in the zone
- whose origin is *origin*, or return *origin* if the successor cannot be
- computed due to name length limitations.
- Note that *origin* is returned in the "too long" cases because wrapping
- around to the origin is how NSEC records express "end of the zone".
- The relativity of the name is preserved, so if this name is relative
- then the method will return a relative name, and likewise if this name
- is absolute then the successor will be absolute.
- *prefix_ok* indicates if prefixing a new minimal label is allowed, and
- defaults to ``True``. Normally it is good to allow this, but if computing
- a minimal successor at a zone cut point then ``False`` must be specified.
- """
- return _handle_relativity_and_call(_absolute_successor, self, origin, prefix_ok)
- #: The root name, '.'
- root = Name([b""])
- #: The empty name.
- empty = Name([])
- def from_unicode(
- text: str, origin: Optional[Name] = root, idna_codec: Optional[IDNACodec] = None
- ) -> Name:
- """Convert unicode text into a Name object.
- Labels are encoded in IDN ACE form according to rules specified by
- the IDNA codec.
- *text*, a ``str``, is the text to convert into a name.
- *origin*, a ``dns.name.Name``, specifies the origin to
- append to non-absolute names. The default is the root name.
- *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
- encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
- is used.
- Returns a ``dns.name.Name``.
- """
- if not isinstance(text, str):
- raise ValueError("input to from_unicode() must be a unicode string")
- if not (origin is None or isinstance(origin, Name)):
- raise ValueError("origin must be a Name or None")
- labels = []
- label = ""
- escaping = False
- edigits = 0
- total = 0
- if idna_codec is None:
- idna_codec = IDNA_2003
- if text == "@":
- text = ""
- if text:
- if text in [".", "\u3002", "\uff0e", "\uff61"]:
- return Name([b""]) # no Unicode "u" on this constant!
- for c in text:
- if escaping:
- if edigits == 0:
- if c.isdigit():
- total = int(c)
- edigits += 1
- else:
- label += c
- escaping = False
- else:
- if not c.isdigit():
- raise BadEscape
- total *= 10
- total += int(c)
- edigits += 1
- if edigits == 3:
- escaping = False
- label += chr(total)
- elif c in [".", "\u3002", "\uff0e", "\uff61"]:
- if len(label) == 0:
- raise EmptyLabel
- labels.append(idna_codec.encode(label))
- label = ""
- elif c == "\\":
- escaping = True
- edigits = 0
- total = 0
- else:
- label += c
- if escaping:
- raise BadEscape
- if len(label) > 0:
- labels.append(idna_codec.encode(label))
- else:
- labels.append(b"")
- if (len(labels) == 0 or labels[-1] != b"") and origin is not None:
- labels.extend(list(origin.labels))
- return Name(labels)
- def is_all_ascii(text: str) -> bool:
- for c in text:
- if ord(c) > 0x7F:
- return False
- return True
- def from_text(
- text: Union[bytes, str],
- origin: Optional[Name] = root,
- idna_codec: Optional[IDNACodec] = None,
- ) -> Name:
- """Convert text into a Name object.
- *text*, a ``bytes`` or ``str``, is the text to convert into a name.
- *origin*, a ``dns.name.Name``, specifies the origin to
- append to non-absolute names. The default is the root name.
- *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
- encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
- is used.
- Returns a ``dns.name.Name``.
- """
- if isinstance(text, str):
- if not is_all_ascii(text):
- # Some codepoint in the input text is > 127, so IDNA applies.
- return from_unicode(text, origin, idna_codec)
- # The input is all ASCII, so treat this like an ordinary non-IDNA
- # domain name. Note that "all ASCII" is about the input text,
- # not the codepoints in the domain name. E.g. if text has value
- #
- # r'\150\151\152\153\154\155\156\157\158\159'
- #
- # then it's still "all ASCII" even though the domain name has
- # codepoints > 127.
- text = text.encode("ascii")
- if not isinstance(text, bytes):
- raise ValueError("input to from_text() must be a string")
- if not (origin is None or isinstance(origin, Name)):
- raise ValueError("origin must be a Name or None")
- labels = []
- label = b""
- escaping = False
- edigits = 0
- total = 0
- if text == b"@":
- text = b""
- if text:
- if text == b".":
- return Name([b""])
- for c in text:
- byte_ = struct.pack("!B", c)
- if escaping:
- if edigits == 0:
- if byte_.isdigit():
- total = int(byte_)
- edigits += 1
- else:
- label += byte_
- escaping = False
- else:
- if not byte_.isdigit():
- raise BadEscape
- total *= 10
- total += int(byte_)
- edigits += 1
- if edigits == 3:
- escaping = False
- label += struct.pack("!B", total)
- elif byte_ == b".":
- if len(label) == 0:
- raise EmptyLabel
- labels.append(label)
- label = b""
- elif byte_ == b"\\":
- escaping = True
- edigits = 0
- total = 0
- else:
- label += byte_
- if escaping:
- raise BadEscape
- if len(label) > 0:
- labels.append(label)
- else:
- labels.append(b"")
- if (len(labels) == 0 or labels[-1] != b"") and origin is not None:
- labels.extend(list(origin.labels))
- return Name(labels)
- # we need 'dns.wire.Parser' quoted as dns.name and dns.wire depend on each other.
- def from_wire_parser(parser: "dns.wire.Parser") -> Name:
- """Convert possibly compressed wire format into a Name.
- *parser* is a dns.wire.Parser.
- Raises ``dns.name.BadPointer`` if a compression pointer did not
- point backwards in the message.
- Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
- Returns a ``dns.name.Name``
- """
- labels = []
- biggest_pointer = parser.current
- with parser.restore_furthest():
- count = parser.get_uint8()
- while count != 0:
- if count < 64:
- labels.append(parser.get_bytes(count))
- elif count >= 192:
- current = (count & 0x3F) * 256 + parser.get_uint8()
- if current >= biggest_pointer:
- raise BadPointer
- biggest_pointer = current
- parser.seek(current)
- else:
- raise BadLabelType
- count = parser.get_uint8()
- labels.append(b"")
- return Name(labels)
- def from_wire(message: bytes, current: int) -> Tuple[Name, int]:
- """Convert possibly compressed wire format into a Name.
- *message* is a ``bytes`` containing an entire DNS message in DNS
- wire form.
- *current*, an ``int``, is the offset of the beginning of the name
- from the start of the message
- Raises ``dns.name.BadPointer`` if a compression pointer did not
- point backwards in the message.
- Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
- Returns a ``(dns.name.Name, int)`` tuple consisting of the name
- that was read and the number of bytes of the wire format message
- which were consumed reading it.
- """
- if not isinstance(message, bytes):
- raise ValueError("input to from_wire() must be a byte string")
- parser = dns.wire.Parser(message, current)
- name = from_wire_parser(parser)
- return (name, parser.current - current)
- # RFC 4471 Support
- _MINIMAL_OCTET = b"\x00"
- _MINIMAL_OCTET_VALUE = ord(_MINIMAL_OCTET)
- _SUCCESSOR_PREFIX = Name([_MINIMAL_OCTET])
- _MAXIMAL_OCTET = b"\xff"
- _MAXIMAL_OCTET_VALUE = ord(_MAXIMAL_OCTET)
- _AT_SIGN_VALUE = ord("@")
- _LEFT_SQUARE_BRACKET_VALUE = ord("[")
- def _wire_length(labels):
- return functools.reduce(lambda v, x: v + len(x) + 1, labels, 0)
- def _pad_to_max_name(name):
- needed = 255 - _wire_length(name.labels)
- new_labels = []
- while needed > 64:
- new_labels.append(_MAXIMAL_OCTET * 63)
- needed -= 64
- if needed >= 2:
- new_labels.append(_MAXIMAL_OCTET * (needed - 1))
- # Note we're already maximal in the needed == 1 case as while we'd like
- # to add one more byte as a new label, we can't, as adding a new non-empty
- # label requires at least 2 bytes.
- new_labels = list(reversed(new_labels))
- new_labels.extend(name.labels)
- return Name(new_labels)
- def _pad_to_max_label(label, suffix_labels):
- length = len(label)
- # We have to subtract one here to account for the length byte of label.
- remaining = 255 - _wire_length(suffix_labels) - length - 1
- if remaining <= 0:
- # Shouldn't happen!
- return label
- needed = min(63 - length, remaining)
- return label + _MAXIMAL_OCTET * needed
- def _absolute_predecessor(name: Name, origin: Name, prefix_ok: bool) -> Name:
- # This is the RFC 4471 predecessor algorithm using the "absolute method" of section
- # 3.1.1.
- #
- # Our caller must ensure that the name and origin are absolute, and that name is a
- # subdomain of origin.
- if name == origin:
- return _pad_to_max_name(name)
- least_significant_label = name[0]
- if least_significant_label == _MINIMAL_OCTET:
- return name.parent()
- least_octet = least_significant_label[-1]
- suffix_labels = name.labels[1:]
- if least_octet == _MINIMAL_OCTET_VALUE:
- new_labels = [least_significant_label[:-1]]
- else:
- octets = bytearray(least_significant_label)
- octet = octets[-1]
- if octet == _LEFT_SQUARE_BRACKET_VALUE:
- octet = _AT_SIGN_VALUE
- else:
- octet -= 1
- octets[-1] = octet
- least_significant_label = bytes(octets)
- new_labels = [_pad_to_max_label(least_significant_label, suffix_labels)]
- new_labels.extend(suffix_labels)
- name = Name(new_labels)
- if prefix_ok:
- return _pad_to_max_name(name)
- else:
- return name
- def _absolute_successor(name: Name, origin: Name, prefix_ok: bool) -> Name:
- # This is the RFC 4471 successor algorithm using the "absolute method" of section
- # 3.1.2.
- #
- # Our caller must ensure that the name and origin are absolute, and that name is a
- # subdomain of origin.
- if prefix_ok:
- # Try prefixing \000 as new label
- try:
- return _SUCCESSOR_PREFIX.concatenate(name)
- except NameTooLong:
- pass
- while name != origin:
- # Try extending the least significant label.
- least_significant_label = name[0]
- if len(least_significant_label) < 63:
- # We may be able to extend the least label with a minimal additional byte.
- # This is only "may" because we could have a maximal length name even though
- # the least significant label isn't maximally long.
- new_labels = [least_significant_label + _MINIMAL_OCTET]
- new_labels.extend(name.labels[1:])
- try:
- return dns.name.Name(new_labels)
- except dns.name.NameTooLong:
- pass
- # We can't extend the label either, so we'll try to increment the least
- # signficant non-maximal byte in it.
- octets = bytearray(least_significant_label)
- # We do this reversed iteration with an explicit indexing variable because
- # if we find something to increment, we're going to want to truncate everything
- # to the right of it.
- for i in range(len(octets) - 1, -1, -1):
- octet = octets[i]
- if octet == _MAXIMAL_OCTET_VALUE:
- # We can't increment this, so keep looking.
- continue
- # Finally, something we can increment. We have to apply a special rule for
- # incrementing "@", sending it to "[", because RFC 4034 6.1 says that when
- # comparing names, uppercase letters compare as if they were their
- # lower-case equivalents. If we increment "@" to "A", then it would compare
- # as "a", which is after "[", "\", "]", "^", "_", and "`", so we would have
- # skipped the most minimal successor, namely "[".
- if octet == _AT_SIGN_VALUE:
- octet = _LEFT_SQUARE_BRACKET_VALUE
- else:
- octet += 1
- octets[i] = octet
- # We can now truncate all of the maximal values we skipped (if any)
- new_labels = [bytes(octets[: i + 1])]
- new_labels.extend(name.labels[1:])
- # We haven't changed the length of the name, so the Name constructor will
- # always work.
- return Name(new_labels)
- # We couldn't increment, so chop off the least significant label and try
- # again.
- name = name.parent()
- # We couldn't increment at all, so return the origin, as wrapping around is the
- # DNSSEC way.
- return origin
- def _handle_relativity_and_call(
- function: Callable[[Name, Name, bool], Name],
- name: Name,
- origin: Name,
- prefix_ok: bool,
- ) -> Name:
- # Make "name" absolute if needed, ensure that the origin is absolute,
- # call function(), and then relativize the result if needed.
- if not origin.is_absolute():
- raise NeedAbsoluteNameOrOrigin
- relative = not name.is_absolute()
- if relative:
- name = name.derelativize(origin)
- elif not name.is_subdomain(origin):
- raise NeedSubdomainOfOrigin
- result_name = function(name, origin, prefix_ok)
- if relative:
- result_name = result_name.relativize(origin)
- return result_name
|