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

48 statements  

1"""Domain.""" 

2 

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 

10 

11# local 

12from .utils import validator 

13 

14 

15class _IanaTLD: 

16 """Read IANA TLDs, and optionally cache them.""" 

17 

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") 

22 

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() 

29 

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 

40 

41 

42def _relies_on_idna_mapping(value: str): 

43 """Return whether `value` only becomes a valid name through IDNA 2003 mapping. 

44 

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 ) 

56 

57 

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. 

63 

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 

72 

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). 

85 

86 

87 Returns: 

88 (Literal[True]): If `value` is a valid domain name. 

89 (ValidationError): If `value` is an invalid domain name. 

90 

91 Raises: 

92 (UnicodeError): If `value` cannot be encoded into `idna` or decoded into `utf-8`. 

93 """ 

94 if not value: 

95 return False 

96 

97 try: 

98 if _relies_on_idna_mapping(value): 

99 return False 

100 

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 

105 

106 service_record = r"_" if rfc_2782 else "" 

107 trailing_dot = r"\.?$" if rfc_1034 else r"$" 

108 

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 

113 

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 

118 

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