Add generic endpoint list parsing
This commit is contained in:
@@ -1,9 +1,84 @@
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from .adapters import Endpoint
|
||||||
from .normalizer import normalize_hostname, normalize_ipv4
|
from .normalizer import normalize_hostname, normalize_ipv4
|
||||||
|
|
||||||
|
|
||||||
|
def parse_endpoint_list(
|
||||||
|
plugin: str,
|
||||||
|
value: str,
|
||||||
|
default_port: int,
|
||||||
|
) -> tuple[Endpoint, ...]:
|
||||||
|
if not 1 <= default_port <= 65535:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Endpoint default port must be between 1 and 65535"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_addresses = value.split(",")
|
||||||
|
if any(not item.strip() for item in raw_addresses):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{plugin.upper()} endpoint list contains an empty address"
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoints: list[Endpoint] = []
|
||||||
|
identities: set[str] = set()
|
||||||
|
|
||||||
|
for raw_address in raw_addresses:
|
||||||
|
address = raw_address.strip().rstrip("/")
|
||||||
|
|
||||||
|
if "://" in address:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{plugin.upper()} endpoint {raw_address!r} must not "
|
||||||
|
"include a URL scheme"
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(character.isspace() for character in address):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{plugin.upper()} endpoint {raw_address!r} "
|
||||||
|
"contains whitespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
parsed = urlsplit(f"//{address}")
|
||||||
|
if (
|
||||||
|
not parsed.hostname
|
||||||
|
or parsed.username
|
||||||
|
or parsed.password
|
||||||
|
or parsed.path
|
||||||
|
or parsed.query
|
||||||
|
or parsed.fragment
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Invalid {plugin.upper()} endpoint address: "
|
||||||
|
f"{raw_address!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
port = parsed.port or default_port
|
||||||
|
except ValueError as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Invalid {plugin.upper()} endpoint port: "
|
||||||
|
f"{raw_address!r}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
host = parsed.hostname.lower()
|
||||||
|
normalized_address = (
|
||||||
|
f"[{host}]:{port}" if ":" in host else f"{host}:{port}"
|
||||||
|
)
|
||||||
|
endpoint = Endpoint(plugin, normalized_address)
|
||||||
|
|
||||||
|
if endpoint.identity in identities:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Duplicate {plugin.upper()} endpoint: {endpoint.address}"
|
||||||
|
)
|
||||||
|
|
||||||
|
identities.add(endpoint.identity)
|
||||||
|
endpoints.append(endpoint)
|
||||||
|
|
||||||
|
return tuple(endpoints)
|
||||||
|
|
||||||
|
|
||||||
def env_bool(name: str, default: bool) -> bool:
|
def env_bool(name: str, default: bool) -> bool:
|
||||||
value = os.getenv(name)
|
value = os.getenv(name)
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from dns_sync.config import parse_endpoint_list
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointParsingTests(unittest.TestCase):
|
||||||
|
def test_comma_separated_addresses_use_default_port(self):
|
||||||
|
endpoints = parse_endpoint_list(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3, 192.168.1.4:8081,192.168.1.5",
|
||||||
|
81,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(endpoint.identity for endpoint in endpoints),
|
||||||
|
(
|
||||||
|
"npm://192.168.1.3:81",
|
||||||
|
"npm://192.168.1.4:8081",
|
||||||
|
"npm://192.168.1.5:81",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_duplicate_is_rejected_after_normalization(self):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "Duplicate NPM"):
|
||||||
|
parse_endpoint_list(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3,192.168.1.3:81",
|
||||||
|
81,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_empty_entry_is_rejected(self):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "empty address"):
|
||||||
|
parse_endpoint_list(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3,,192.168.1.5",
|
||||||
|
81,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_url_scheme_is_rejected(self):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "must not include"):
|
||||||
|
parse_endpoint_list(
|
||||||
|
"npm",
|
||||||
|
"http://192.168.1.3:81",
|
||||||
|
81,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user