"""Platform-wide validation rules carried over from the existing infrastructure. These are pure functions with no I/O, so both the application layer *and* the adapters can enforce them. Defence in depth: a rule that is only checked in one place is a rule that will eventually be bypassed. Reference: ``docs/current-infra-notes.md`` §1. """ from __future__ import annotations import re DEFAULT_ROOT_DOMAIN = "msa.artacloud.ir" #: Ports that are never allocatable (system services + data stores). SYSTEM_RESERVED_PORTS: frozenset[int] = frozenset( {22, 53, 80, 443, 2019, 3000, 5432, 6379, 8008} ) #: Ports of internal platform services. A tenant preview must never proxy to #: these, otherwise a public hostname would expose another component. SYSTEM_PROTECTED_PORTS: frozenset[int] = ( frozenset({3000, 35555, 35556, 35557}) | SYSTEM_RESERVED_PORTS ) DEFAULT_PORT_RANGE_START = 8080 DEFAULT_PORT_RANGE_END = 8999 LABEL_MIN = 3 LABEL_MAX = 40 _LABEL_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$") _PROXY_TARGET_RE = re.compile(r"^(https?://)?(localhost|127\.0\.0\.1):(\d{1,5})$") class RuleViolation(ValueError): """Raised when a request violates a platform safety rule.""" def validate_subdomain_label(label: str) -> str: """Normalise and validate a single DNS label. Only ``a-z0-9-``, must start and end alphanumeric, 3-40 characters. """ clean = (label or "").strip().lower() if not (LABEL_MIN <= len(clean) <= LABEL_MAX): raise RuleViolation( f"نام زیردامنه باید بین {LABEL_MIN} تا {LABEL_MAX} کاراکتر باشد." ) if not _LABEL_RE.match(clean): raise RuleViolation("نام زیردامنه فقط می‌تواند شامل حروف کوچک، عدد و خط تیره باشد.") return clean def build_host(label: str, root_domain: str = DEFAULT_ROOT_DOMAIN) -> str: return f"{validate_subdomain_label(label)}.{root_domain.strip().lower()}" def validate_managed_host(host: str, root_domain: str = DEFAULT_ROOT_DOMAIN) -> str: """Validate a fully-qualified host against the managed wildcard domain. Rejects anything that is not a single label under the root domain — this is the anti-injection check that keeps a crafted host from breaking out of a Caddy site block or pointing at an arbitrary host. """ clean = (host or "").strip().lower() suffix = f".{root_domain.strip().lower()}" if not clean.endswith(suffix): raise RuleViolation(f"دامنه باید زیرمجموعهٔ «{root_domain}» باشد.") label = clean[: -len(suffix)] if "." in label: raise RuleViolation("فقط یک برچسب (single label) مجاز است.") validate_subdomain_label(label) return clean def validate_proxy_target(target: str) -> tuple[str, int]: """Ensure the reverse-proxy target is a loopback address with a numeric port. Blocks SSRF: a tenant must not be able to proxy a public hostname to an internal service. """ clean = (target or "").strip() match = _PROXY_TARGET_RE.match(clean) if not match: raise RuleViolation( "مقصد reverse proxy باید localhost یا 127.0.0.1 با پورت عددی باشد." ) port = int(match.group(3)) if not (1 <= port <= 65535): raise RuleViolation("شمارهٔ پورت نامعتبر است.") return match.group(2), port def is_port_allocatable( port: int, *, start: int = DEFAULT_PORT_RANGE_START, end: int = DEFAULT_PORT_RANGE_END, ) -> bool: if port in SYSTEM_RESERVED_PORTS: return False return start <= port <= end def assert_port_allocatable( port: int, *, start: int = DEFAULT_PORT_RANGE_START, end: int = DEFAULT_PORT_RANGE_END, ) -> int: if not is_port_allocatable(port, start=start, end=end): raise RuleViolation(f"پورت {port} قابل تخصیص نیست.") return port def assert_not_protected_port(port: int) -> int: """A preview domain must never point at an internal platform port.""" if port in SYSTEM_PROTECTED_PORTS: raise RuleViolation(f"اتصال به پورت محافظت‌شدهٔ {port} ممنوع است.") return port