Skip to content

Commit 7b6ddc5

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 8a5b260 commit 7b6ddc5

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
@@ -93,9 +93,6 @@ def domain(
9393
if not value:
9494
return False
9595

96-
if consider_tld and not _IanaTLD.check(value.rstrip(".").rsplit(".", 1)[-1].upper()):
97-
return False
98-
9996
try:
10097
if _relies_on_idna_mapping(value):
10198
return False
@@ -108,6 +105,11 @@ def domain(
108105
# ref: RFC 1035, presentation form is limited to 253 characters
109106
return False
110107

108+
# IANA lists IDN TLDs in their ASCII (punycode) form,
109+
# so look up the encoded TLD, not the raw one.
110+
if consider_tld and not _IanaTLD.check(encoded.rstrip(".").rsplit(".", 1)[-1].upper()):
111+
return False
112+
111113
return not re.search(r"\s|__+", value) and re.match(
112114
# First character of the domain
113115
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
@@ -137,3 +137,29 @@ def test_returns_failed_validation_on_non_ascii_simple_hostname(value: str):
137137
def test_returns_failed_validation_on_idna_mapped_lookalike_hostname(value: str):
138138
"""Test returns failed validation on hostnames that only IDNA 2003 mapping makes valid."""
139139
assert isinstance(hostname(value), ValidationError)
140+
141+
142+
@pytest.mark.parametrize(
143+
"value",
144+
[
145+
"example.com:443",
146+
"xn--e1afmkfd.xn--p1ai",
147+
"пример.рф",
148+
"пример.рф:8080",
149+
],
150+
)
151+
def test_returns_true_on_valid_top_level_domain_hostname(value: str):
152+
"""Test returns true on hostname with a TLD allowed by IANA."""
153+
assert hostname(value, consider_tld=True)
154+
155+
156+
@pytest.mark.parametrize(
157+
"value",
158+
[
159+
"example.flat:443",
160+
"пример.рфы",
161+
],
162+
)
163+
def test_returns_failed_validation_on_invalid_top_level_domain_hostname(value: str):
164+
"""Test returns failed validation on hostname with a TLD not allowed by IANA."""
165+
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
@@ -266,3 +266,29 @@ def test_url_accepts_percent_encoded_controls(encoded: str, template: str):
266266
def test_returns_failed_validation_on_idna_mapped_lookalike_url(value: str):
267267
"""Test returns failed validation on url hosts that only IDNA 2003 mapping makes valid."""
268268
assert isinstance(url(value, simple_host=True), ValidationError)
269+
270+
271+
@pytest.mark.parametrize(
272+
"value",
273+
[
274+
"https://example.com/path",
275+
"https://xn--e1afmkfd.xn--p1ai/",
276+
"https://пример.рф/",
277+
"http://例子.中国:8080/index.html",
278+
],
279+
)
280+
def test_returns_true_on_valid_top_level_domain_url(value: str):
281+
"""Test returns true on url with a TLD allowed by IANA."""
282+
assert url(value, consider_tld=True)
283+
284+
285+
@pytest.mark.parametrize(
286+
"value",
287+
[
288+
"https://example.flat/",
289+
"https://пример.рфы/",
290+
],
291+
)
292+
def test_returns_failed_validation_on_invalid_top_level_domain_url(value: str):
293+
"""Test returns failed validation on url with a TLD not allowed by IANA."""
294+
assert isinstance(url(value, consider_tld=True), ValidationError)

0 commit comments

Comments
 (0)