from __future__ import annotations import ipaddress import socket from urllib.parse import urlparse _PRIVATE_NETWORKS = [ 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("fc00::/7"), ipaddress.ip_network("::1/128"), ] def is_private_url(url: str) -> bool: """检查 URL 是否指向内网地址 — SSRF 防护。""" if not url: return False try: parsed = urlparse(url) hostname = parsed.hostname if not hostname: return False if hostname in ("localhost", "localhost.localdomain"): return True addr = ipaddress.ip_address(hostname) for network in _PRIVATE_NETWORKS: if addr in network: return True return False except ValueError: try: resolved = socket.getaddrinfo(hostname, None) for family, _, _, _, sockaddr in resolved: ip = sockaddr[0] addr = ipaddress.ip_address(ip) for network in _PRIVATE_NETWORKS: if addr in network: return True except socket.gaierror: pass return False return False def validate_url_safety(url: str, dangerously_allow_private: bool = False) -> tuple[bool, str]: """验证 URL 安全性。返回 (is_safe, reason)。""" if not url: return False, "URL is empty" parsed = urlparse(url) if parsed.scheme not in ("http", "https"): return False, f"Unsupported scheme: {parsed.scheme}" if not dangerously_allow_private and is_private_url(url): return False, f"URL points to private/internal network: {parsed.hostname}" return True, "ok" def check_dangerously_allow_private_network(config: dict) -> bool: return bool(config.get("network", {}).get("dangerouslyAllowPrivateNetwork", False)) def normalize_mattermost_base_url(url: str) -> str: url = url.strip() url = url.rstrip("/") if url.endswith("/api/v4"): url = url[: -len("/api/v4")] return url.rstrip("/") def safe_url_for_driver(url: str) -> str: return normalize_mattermost_base_url(url)