Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/validators/domain.py: 75%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Domain."""
3# standard
4from os import environ
5from pathlib import Path
6import re
7from stringprep import in_table_b1
8from typing import Optional, Set
9from unicodedata import normalize
11# local
12from .utils import validator
15class _IanaTLD:
16 """Read IANA TLDs, and optionally cache them."""
18 _full_cache: Optional[Set[str]] = None
19 # source: https://www.statista.com/statistics/265677
20 _popular_cache = {"COM", "ORG", "RU", "DE", "NET", "BR", "UK", "JP", "FR", "IT"}
21 _popular_cache.add("ONION")
23 @classmethod
24 def _retrieve(cls):
25 with Path(__file__).parent.joinpath("_tld.txt").open() as tld_f:
26 _ = next(tld_f) # ignore the first line
27 for line in tld_f:
28 yield line.strip()
30 @classmethod
31 def check(cls, tld: str):
32 if tld in cls._popular_cache:
33 return True
34 if cls._full_cache is None:
35 if environ.get("PYVLD_CACHE_TLD") == "True":
36 cls._full_cache = set(cls._retrieve())
37 else:
38 return tld in cls._retrieve()
39 return tld in cls._full_cache
42def _relies_on_idna_mapping(value: str):
43 """Return whether `value` only becomes a valid name through IDNA 2003 mapping.
45 Python's `idna` codec implements IDNA 2003 (RFC 3490), whose nameprep step
46 (RFC 3491) drops invisible characters (RFC 3454, table B.1), folds
47 compatibility characters such as fullwidth letters to ASCII, and treats
48 U+3002 and similar dots as label separators. A value that needs any of
49 these only looks like the name it encodes to, e.g. "example.com" written
50 with fullwidth letters.
51 """
52 return not value.isascii() and (
53 normalize("NFKC", value) != value
54 or any(in_table_b1(char) or char in "\u3002\uff0e\uff61" for char in value)
55 )
58@validator
59def domain(
60 value: str, /, *, consider_tld: bool = False, rfc_1034: bool = False, rfc_2782: bool = False
61):
62 """Return whether or not given value is a valid domain.
64 Examples:
65 >>> domain('example.com')
66 True
67 >>> domain('example.com/')
68 ValidationError(func=domain, args={'value': 'example.com/'})
69 >>> # Supports IDN domains as well::
70 >>> domain('xn----gtbspbbmkef.xn--p1ai')
71 True
73 Args:
74 value:
75 Domain string to validate.
76 consider_tld:
77 Restrict domain to TLDs allowed by IANA.
78 rfc_1034:
79 Allows optional trailing dot in the domain name.
80 Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
81 rfc_2782:
82 Domain name is of type service record.
83 Allows optional underscores in the domain name.
84 Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782).
87 Returns:
88 (Literal[True]): If `value` is a valid domain name.
89 (ValidationError): If `value` is an invalid domain name.
91 Raises:
92 (UnicodeError): If `value` cannot be encoded into `idna` or decoded into `utf-8`.
93 """
94 if not value:
95 return False
97 try:
98 if _relies_on_idna_mapping(value):
99 return False
101 if value.rstrip(".").rsplit(".", 1)[-1].isdecimal():
102 # ref: RFC 1123 (2.1) and RFC 3696 (2), a TLD is not all-numeric. An
103 # all-digit IDN label like "\u0661" is, but encodes to a letter-final A-label.
104 return False
106 service_record = r"_" if rfc_2782 else ""
107 trailing_dot = r"\.?$" if rfc_1034 else r"$"
109 encoded = value.encode("idna").decode("utf-8")
110 if len(encoded.rstrip(".")) > 253:
111 # ref: RFC 1035, presentation form is limited to 253 characters
112 return False
114 # IANA lists IDN TLDs in their ASCII (punycode) form,
115 # so look up the encoded TLD, not the raw one.
116 if consider_tld and not _IanaTLD.check(encoded.rstrip(".").rsplit(".", 1)[-1].upper()):
117 return False
119 return not re.search(r"\s|__+", value) and re.match(
120 # First character of the domain
121 rf"^(?:[a-z0-9{service_record}]"
122 # Sub-domain
123 + rf"(?:[a-z0-9-{service_record}]{{0,61}}"
124 # Hostname
125 + rf"[a-z0-9{service_record}])?\.)"
126 # First 61 characters of the gTLD. An underscore belongs
127 # in a service label, not here.
128 + r"+[a-z0-9][a-z0-9-]{0,61}"
129 # Last character of the gTLD
130 + rf"[a-z]{trailing_dot}",
131 encoded,
132 re.IGNORECASE,
133 )
134 except UnicodeError as err:
135 raise UnicodeError(f"Unable to encode/decode {value}") from err