96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
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)
|