diff --git a/src/validators/domain.py b/src/validators/domain.py index 1ce4a262..df0ebe59 100644 --- a/src/validators/domain.py +++ b/src/validators/domain.py @@ -4,7 +4,9 @@ from os import environ from pathlib import Path import re +from stringprep import in_table_b1 from typing import Optional, Set +from unicodedata import normalize # local from .utils import validator @@ -37,6 +39,21 @@ def check(cls, tld: str): return tld in cls._full_cache +def _relies_on_idna_mapping(value: str): + """Return whether `value` only becomes a valid name through IDNA 2003 mapping. + + Python's `idna` codec implements IDNA 2003 (RFC 3490), whose nameprep step + (RFC 3491) drops invisible characters (RFC 3454, table B.1), folds + compatibility characters such as fullwidth letters to ASCII, and treats + U+3002 and similar dots as label separators. A value that needs any of + these only looks like the name it encodes to, e.g. "example.com". + """ + return not value.isascii() and ( + normalize("NFKC", value) != value + or any(in_table_b1(char) or char in "\u3002\uff0e\uff61" for char in value) + ) + + @validator def domain( value: str, /, *, consider_tld: bool = False, rfc_1034: bool = False, rfc_2782: bool = False @@ -80,6 +97,9 @@ def domain( return False try: + if _relies_on_idna_mapping(value): + return False + service_record = r"_" if rfc_2782 else "" trailing_dot = r"\.?$" if rfc_1034 else r"$" diff --git a/src/validators/hostname.py b/src/validators/hostname.py index ad415e13..e34ab7e4 100644 --- a/src/validators/hostname.py +++ b/src/validators/hostname.py @@ -26,7 +26,9 @@ def _simple_hostname_regex(): """Simple hostname validation regex.""" # {0,59} because two characters are already matched at # the beginning and at the end, making the range {1, 61} - return re.compile(r"^(?!-)[a-z0-9](?:[a-z0-9-]{0,59}[a-z0-9])?(?