Implement webhook
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""Validate webhook destinations before every outbound request."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import socket
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
class UnsafeWebhookUrl(ValueError):
|
||||
"""Raised when a URL could reach local or otherwise non-public infrastructure."""
|
||||
|
||||
def __init__(self, message: str, *, retryable: bool = False):
|
||||
super().__init__(message)
|
||||
self.retryable = retryable
|
||||
|
||||
|
||||
def _public_ip(address: str) -> bool:
|
||||
try:
|
||||
return ipaddress.ip_address(address).is_global
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
async def validate_webhook_url(url: str) -> str:
|
||||
"""Require HTTPS and ensure every currently resolved address is public."""
|
||||
value = url.strip()
|
||||
parsed = urlsplit(value)
|
||||
if parsed.scheme != "https" or not parsed.hostname:
|
||||
raise UnsafeWebhookUrl("Webhook URL 必须是完整的 HTTPS 地址")
|
||||
if parsed.username or parsed.password:
|
||||
raise UnsafeWebhookUrl("Webhook URL 不能包含用户名或密码")
|
||||
|
||||
try:
|
||||
port = parsed.port or 443
|
||||
except ValueError as exc:
|
||||
raise UnsafeWebhookUrl("Webhook URL 端口无效") from exc
|
||||
|
||||
try:
|
||||
direct_ip = ipaddress.ip_address(parsed.hostname)
|
||||
except ValueError:
|
||||
direct_ip = None
|
||||
if direct_ip is not None:
|
||||
if not direct_ip.is_global:
|
||||
raise UnsafeWebhookUrl("Webhook URL 不能指向内网或本机地址")
|
||||
return value
|
||||
|
||||
try:
|
||||
records = await asyncio.to_thread(
|
||||
socket.getaddrinfo,
|
||||
parsed.hostname,
|
||||
port,
|
||||
type=socket.SOCK_STREAM,
|
||||
)
|
||||
except OSError as exc:
|
||||
raise UnsafeWebhookUrl("Webhook 域名暂时无法解析", retryable=True) from exc
|
||||
addresses = {str(record[4][0]) for record in records}
|
||||
if not addresses or any(not _public_ip(address) for address in addresses):
|
||||
raise UnsafeWebhookUrl("Webhook 域名解析到了内网或非公网地址")
|
||||
return value
|
||||
Reference in New Issue
Block a user