from __future__ import annotations import ipaddress from typing import Any from urllib.parse import urlparse import httpx from yuxi.channels.exceptions import ChannelConnectionError from yuxi.utils.logging_config import logger from .client import UrbitClient _PRIVATE_NETS = [ ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("172.16.0.0/12"), ipaddress.ip_network("192.168.0.0/16"), ipaddress.ip_network("127.0.0.0/8"), ipaddress.ip_network("169.254.0.0/16"), ipaddress.ip_network("::1/128"), ipaddress.ip_network("fc00::/7"), ipaddress.ip_network("fe80::/10"), ] def validate_urbit_url(url: str, allow_private: bool = False) -> tuple[bool, list[str]]: warnings: list[str] = [] if not url: warnings.append("URL is empty") return False, warnings if not url.startswith(("http://", "https://")): warnings.append(f"URL must start with http:// or https://, got: {url}") return False, warnings try: parsed = urlparse(url) except Exception: warnings.append(f"Invalid URL format: {url}") return False, warnings hostname = parsed.hostname if not hostname: warnings.append(f"URL has no hostname: {url}") return False, warnings if "@" in parsed.netloc.split("@")[0] if "@" in parsed.netloc else False: warnings.append("URL contains credentials in the host portion") try: hostname_stripped = hostname.strip("[]") addr = ipaddress.ip_address(hostname_stripped) for net in _PRIVATE_NETS: if addr in net: if allow_private: warnings.append(f"Host {hostname} is a private network address (allowed by config)") else: warnings.append( f"Host {hostname} is a private network address. Set dangerously_allow_private_network=true to bypass" ) return False, warnings break except ValueError: pass return True, warnings async def probe_ship(client: UrbitClient) -> dict[str, object]: try: r = await client.get("/~/meta") r.raise_for_status() logger.debug(f"[Urbit] Ship ~{client.ship_name} reachable") return {"reachable": True, "url": client.ship_url} except httpx.RequestError as e: raise ChannelConnectionError(f"Ship ~{client.ship_name} unreachable") from e def ssrf_policy_from_dangerously_allow_private_network( dangerously_allow_private_network: bool = False, ) -> dict[str, Any]: return { "dangerouslyAllowPrivateNetwork": dangerously_allow_private_network, "privateNets": [str(net) for net in _PRIVATE_NETS], "allowPrivate": dangerously_allow_private_network, } def fetch_with_ssrf_guard( url: str, dangerously_allow_private_network: bool = False, ) -> tuple[bool, list[str]]: return validate_urbit_url(url, allow_private=dangerously_allow_private_network)