新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
82 lines
2.3 KiB
Python
82 lines
2.3 KiB
Python
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)
|