مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
"""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
|