77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""Safety checks for opt-in e2e-ui external server reuse."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ipaddress
|
|
import socket
|
|
from urllib.parse import urlparse
|
|
|
|
DEV_PORTS = frozenset({6767, 8000, 5173})
|
|
# Literal hostnames are a fast path; resolved IPs are classified by _dev_address_reason.
|
|
DEV_HOSTNAMES = frozenset({"localhost", "0.0.0.0"})
|
|
|
|
|
|
def unsafe_ui_base_url_reason(base_url: str) -> str | None:
|
|
"""Return why ``base_url`` looks unsafe for shared e2e-ui reuse.
|
|
|
|
``--ui-base-url`` points tests at a server this pytest session did not
|
|
spawn. Known dev ports and local/private hosts are refused by default so a
|
|
run cannot silently share a developer server, database, or artifact store.
|
|
"""
|
|
parsed = urlparse(base_url)
|
|
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
return "it must be an absolute http(s) URL with a host"
|
|
|
|
host = parsed.hostname.rstrip(".").lower()
|
|
try:
|
|
port = parsed.port
|
|
except ValueError:
|
|
return "it has an invalid port"
|
|
# No explicit port is not itself unsafe; host checks below still apply.
|
|
if port is not None and port in DEV_PORTS:
|
|
return f"port {port} is a known Omnigent/Vite dev port"
|
|
if host in DEV_HOSTNAMES:
|
|
return f"host {host!r} is a local dev host"
|
|
|
|
host_reason = _dev_host_reason(host)
|
|
if host_reason is not None:
|
|
return host_reason
|
|
return None
|
|
|
|
|
|
def _dev_host_reason(host: str) -> str | None:
|
|
try:
|
|
address = ipaddress.ip_address(host)
|
|
except ValueError:
|
|
return _resolved_dev_host_reason(host)
|
|
return _dev_address_reason(address)
|
|
|
|
|
|
def _resolved_dev_host_reason(host: str) -> str | None:
|
|
try:
|
|
infos = socket.getaddrinfo(host, None, type=socket.SOCK_STREAM)
|
|
except socket.gaierror:
|
|
return None
|
|
for info in infos:
|
|
address_text = info[4][0]
|
|
try:
|
|
address = ipaddress.ip_address(address_text)
|
|
except ValueError:
|
|
continue
|
|
reason = _dev_address_reason(address)
|
|
if reason is not None:
|
|
return f"host {host!r} resolves to {address_text}, {reason}"
|
|
return None
|
|
|
|
|
|
def _dev_address_reason(address: ipaddress.IPv4Address | ipaddress.IPv6Address) -> str | None:
|
|
if address.is_loopback:
|
|
return "a loopback address"
|
|
if address.is_unspecified:
|
|
return "an unspecified local address"
|
|
if address.is_link_local:
|
|
return "a link-local address"
|
|
if address.is_private:
|
|
return "a private-network address"
|
|
return None
|