Skip to content

Commit 867054a

Browse files
committed
Look up the IDNA-encoded TLD when consider_tld is set
The IANA list stores internationalized TLDs in punycode (XN--P1AI), but domain() checked the raw Unicode TLD against it. Unicode domains such as пример.рф were rejected with consider_tld=True while their punycode form was accepted. hostname() and url() inherit this through domain(). Run the TLD check after IDNA encoding, on the encoded TLD.
1 parent 6ffd862 commit 867054a

4 files changed

Lines changed: 61 additions & 3 deletions

File tree

‎src/validators/domain.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ def domain(
7676
if not value:
7777
return False
7878

79-
if consider_tld and not _IanaTLD.check(value.rstrip(".").rsplit(".", 1)[-1].upper()):
80-
return False
81-
8279
try:
8380
service_record = r"_" if rfc_2782 else ""
8481
trailing_dot = r"\.?$" if rfc_1034 else r"$"
@@ -88,6 +85,11 @@ def domain(
8885
# ref: RFC 1035, presentation form is limited to 253 characters
8986
return False
9087

88+
# IANA lists IDN TLDs in their ASCII (punycode) form,
89+
# so look up the encoded TLD, not the raw one.
90+
if consider_tld and not _IanaTLD.check(encoded.rstrip(".").rsplit(".", 1)[-1].upper()):
91+
return False
92+
9193
return not re.search(r"\s|__+", value) and re.match(
9294
# First character of the domain
9395
rf"^(?:[a-z0-9{service_record}]"

‎tests/test_domain.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def test_returns_true_on_valid_domain(value: str, rfc_1034: bool, rfc_2782: bool
4747
("example_.com", True, False, True),
4848
("somerandomexample.xn--fiqs8s", True, False, False),
4949
("somerandomexample.onion", True, False, False),
50+
("пример.рф", True, False, False),
51+
("пример.рф.", True, True, False),
52+
("例子.中国", True, False, False),
5053
],
5154
)
5255
def test_returns_true_on_valid_top_level_domain(
@@ -100,6 +103,7 @@ def test_returns_failed_validation_on_invalid_domain(value: str, rfc_1034: bool,
100103
("_example.#13", True, False, True),
101104
("example_.fo-ul", True, False, True),
102105
("somerandomexample.xn-n-fiqs8s", True, False, False),
106+
("пример.рфы", True, False, False),
103107
],
104108
)
105109
def test_returns_failed_validation_invalid_top_level_domain(

‎tests/test_hostname.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,29 @@ def test_returns_true_on_hostname_within_rfc_length_limits(value: str):
109109
def test_returns_failed_validation_on_hostname_beyond_rfc_limits(value: str):
110110
"""Test returns failed validation on hostname beyond RFC limits."""
111111
assert isinstance(hostname(value), ValidationError)
112+
113+
114+
@pytest.mark.parametrize(
115+
"value",
116+
[
117+
"example.com:443",
118+
"xn--e1afmkfd.xn--p1ai",
119+
"пример.рф",
120+
"пример.рф:8080",
121+
],
122+
)
123+
def test_returns_true_on_valid_top_level_domain_hostname(value: str):
124+
"""Test returns true on hostname with a TLD allowed by IANA."""
125+
assert hostname(value, consider_tld=True)
126+
127+
128+
@pytest.mark.parametrize(
129+
"value",
130+
[
131+
"example.flat:443",
132+
"пример.рфы",
133+
],
134+
)
135+
def test_returns_failed_validation_on_invalid_top_level_domain_hostname(value: str):
136+
"""Test returns failed validation on hostname with a TLD not allowed by IANA."""
137+
assert isinstance(hostname(value, consider_tld=True), ValidationError)

‎tests/test_url.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,29 @@ def test_returns_failed_validation_on_invalid_private_url(value: str, private: O
232232
def test_returns_string_for_valid_file_url(value: str):
233233
"""Test returns true on valid file url. Ref: RFC 8089."""
234234
assert url(value)
235+
236+
237+
@pytest.mark.parametrize(
238+
"value",
239+
[
240+
"https://example.com/path",
241+
"https://xn--e1afmkfd.xn--p1ai/",
242+
"https://пример.рф/",
243+
"http://例子.中国:8080/index.html",
244+
],
245+
)
246+
def test_returns_true_on_valid_top_level_domain_url(value: str):
247+
"""Test returns true on url with a TLD allowed by IANA."""
248+
assert url(value, consider_tld=True)
249+
250+
251+
@pytest.mark.parametrize(
252+
"value",
253+
[
254+
"https://example.flat/",
255+
"https://пример.рфы/",
256+
],
257+
)
258+
def test_returns_failed_validation_on_invalid_top_level_domain_url(value: str):
259+
"""Test returns failed validation on url with a TLD not allowed by IANA."""
260+
assert isinstance(url(value, consider_tld=True), ValidationError)

0 commit comments

Comments
 (0)