From: Aleš Mrázek Date: Mon, 11 Apr 2022 20:52:01 +0000 (+0200) Subject: manager: datamodel: types: punycode for DomainName X-Git-Tag: v6.0.0a1~37^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7b8a69ca73afad796077ad5bf1d92cb11f9e87f2;p=thirdparty%2Fknot-resolver.git manager: datamodel: types: punycode for DomainName --- diff --git a/manager/knot_resolver_manager/datamodel/types/types.py b/manager/knot_resolver_manager/datamodel/types/types.py index 1d2806c4c..73603f2c9 100644 --- a/manager/knot_resolver_manager/datamodel/types/types.py +++ b/manager/knot_resolver_manager/datamodel/types/types.py @@ -55,17 +55,57 @@ class TimeUnit(UnitBase): return self._value -class DomainName(PatternBase): - _spec_chars = "ßàÁâãóôþüúðæåïçèõöÿýòäœêëìíøùîûñé" +class DomainName(StrBase): + """ + Fully or partially qualified domain name. + """ + _re = re.compile( - # max 253 chars - r"(?=^.{,253}$)" - # do not start/end with dash; 1-63 chars in name; allow special chars; max 126 levels+TLD - rf"^((?!-)([{_spec_chars}]|[a-zA-Z0-9-]){{1,62}}[a-zA-Z0-9]\.){{0,126}}" - # TLD - r"[a-zA-Z]{2,6}($|.$)" + r"(?=^.{,253}$)" # max 253 chars + r"^([a-zA-Z0-9]" # do not start with hyphen + r"([a-zA-Z0-9-]){1,61}" # max 63 chars in label + r"[a-zA-Z0-9]\.)" # do not end with hyphen + r"{0,126}" # max 126 levels+TLD + r"([a-zA-Z]){2,6}($|.$)" # TLD; end with or without '.' ) + def __init__(self, source_value: Any, object_path: str = "/") -> None: + super().__init__(source_value) + if isinstance(source_value, str): + try: + punycode = source_value.encode("idna").decode("utf-8") + except ValueError: + raise SchemaException( + f"conversion of '{source_value}' to IDN punycode representation failed", + object_path, + ) + + if type(self)._re.match(punycode): + self._value = source_value + else: + raise SchemaException( + f"'{source_value}' represented in punycode '{punycode}' does not match '{self._re.pattern}' pattern", + object_path, + ) + else: + raise SchemaException( + "Unexpected value for ''." + f" Expected string, got '{source_value}' with type '{type(source_value)}'", + object_path, + ) + + def __hash__(self) -> int: + if self._value.endswith("."): + return hash(self._value) + return hash(f"{self._value}.") + + def punycode(self) -> bytes: + return self._value.encode("idna") + + @classmethod + def json_schema(cls: Type["DomainName"]) -> Dict[Any, Any]: + return {"type": "string", "pattern": rf"{cls._re.pattern}"} + class InterfaceName(PatternBase): _re = re.compile(r"^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$") diff --git a/manager/tests/unit/datamodel/types/test_custom_types.py b/manager/tests/unit/datamodel/types/test_custom_types.py index 2a0ec429c..2130d8acf 100644 --- a/manager/tests/unit/datamodel/types/test_custom_types.py +++ b/manager/tests/unit/datamodel/types/test_custom_types.py @@ -83,11 +83,12 @@ def test_checked_path(): assert str(TestSchema({"p": "/tmp"}).p) == "/tmp" -@pytest.mark.parametrize("val", ["example.com.", "test.example.com", "test-example.com"]) +@pytest.mark.parametrize("val", ["example.com.", "test.example.com", "test-example.com", "bücher.com.", "příklad.cz"]) def test_domain_name_valid(val: str): o = DomainName(val) assert str(o) == val assert o == DomainName(val) + assert o.punycode() == val.encode("idna") @pytest.mark.parametrize("val", ["test.example.com..", "-example.com", "test-.example.net"])