新增设备身份管理、认证限流、并发通道、Webhook路由、RBAC权限控制、SSE/轮询降级等全套网关通道功能,包含: 1. 设备身份生成与签名验证 2. 设备令牌认证与速率限制 3. 内存+数据库双重设备注册表 4. 并发通道限流管理 5. Webhook安全处理与路由 6. RBAC权限校验系统 7. OpenAI API兼容适配层 8. Tailscale认证支持 9. HTTP轮询降级机制
162 lines
4.8 KiB
Python
162 lines
4.8 KiB
Python
import asyncio
|
||
import logging
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
from yuxi.channel.gateway.protocol import DeliveryMode
|
||
from yuxi.channel.gateway.webhook_security import (
|
||
WebhookGuard,
|
||
WebhookGuardConfig,
|
||
WebhookGuardResult,
|
||
)
|
||
from yuxi.channel.gateway.webhook_security import (
|
||
webhook_guard as _default_webhook_guard,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
WebhookHandler = Callable[[dict[str, Any]], asyncio.Future[dict[str, Any]]]
|
||
|
||
|
||
class WebhookRegistry:
|
||
"""渠道 Webhook 处理器注册表。
|
||
|
||
每个渠道插件注册自己的 webhook 处理函数,
|
||
HTTP 层统一通过 FastAPI router 将请求分发到此处。
|
||
|
||
集成了多层 Webhook 安全 Guard:
|
||
- Method 检查(仅允许注册的 HTTP 方法)
|
||
- Content-Type 验证
|
||
- Body 大小限制
|
||
- HMAC-SHA256 签名校验
|
||
- 并发限制
|
||
- 异常追踪告警
|
||
"""
|
||
|
||
def __init__(self, webhook_guard: WebhookGuard | None = None):
|
||
self._handlers: dict[str, WebhookHandler] = {}
|
||
self._delivery_modes: dict[str, DeliveryMode] = {}
|
||
self._guard = webhook_guard or _default_webhook_guard
|
||
|
||
def register(
|
||
self,
|
||
channel_type: str,
|
||
handler: WebhookHandler,
|
||
delivery_mode: DeliveryMode = DeliveryMode.DIRECT,
|
||
guard_config: WebhookGuardConfig | None = None,
|
||
) -> None:
|
||
if channel_type in self._handlers:
|
||
logger.warning("Overwriting webhook handler for channel: %s", channel_type)
|
||
self._handlers[channel_type] = handler
|
||
self._delivery_modes[channel_type] = delivery_mode
|
||
self._guard.register_channel(channel_type, guard_config)
|
||
|
||
def get_handler(self, channel_type: str) -> WebhookHandler | None:
|
||
return self._handlers.get(channel_type)
|
||
|
||
def get_delivery_mode(self, channel_type: str) -> DeliveryMode:
|
||
return self._delivery_modes.get(channel_type, DeliveryMode.DIRECT)
|
||
|
||
def list_channels(self) -> list[dict[str, str]]:
|
||
return [
|
||
{
|
||
"channel_type": ct,
|
||
"delivery_mode": self._delivery_modes.get(ct, DeliveryMode.DIRECT).value,
|
||
}
|
||
for ct in self._handlers
|
||
]
|
||
|
||
def remove(self, channel_type: str) -> None:
|
||
self._handlers.pop(channel_type, None)
|
||
self._delivery_modes.pop(channel_type, None)
|
||
self._guard.unregister_channel(channel_type)
|
||
|
||
def clear(self) -> None:
|
||
for ct in list(self._handlers):
|
||
self._guard.unregister_channel(ct)
|
||
self._handlers.clear()
|
||
self._delivery_modes.clear()
|
||
|
||
async def dispatch_guarded(
|
||
self,
|
||
channel_type: str,
|
||
method: str,
|
||
content_type: str | None,
|
||
body: bytes,
|
||
signature: str | None = None,
|
||
extra_headers: dict[str, str] | None = None,
|
||
) -> tuple[WebhookGuardResult | None, dict[str, Any] | None]:
|
||
guard_result = self._guard.run_pipeline(
|
||
channel_type,
|
||
method,
|
||
content_type,
|
||
body,
|
||
signature,
|
||
)
|
||
if not guard_result.allowed:
|
||
return guard_result, None
|
||
|
||
handler = self._handlers.get(channel_type)
|
||
if handler is None:
|
||
self._guard.release_concurrency(channel_type)
|
||
return WebhookGuardResult(
|
||
allowed=False,
|
||
error_code=None,
|
||
error_message=f"No handler registered for channel: {channel_type}",
|
||
http_status=404,
|
||
), None
|
||
|
||
try:
|
||
import json
|
||
|
||
payload = json.loads(body)
|
||
except Exception:
|
||
self._guard.release_concurrency(channel_type)
|
||
return WebhookGuardResult(
|
||
allowed=False,
|
||
error_code=None,
|
||
error_message="Invalid JSON payload",
|
||
http_status=400,
|
||
), None
|
||
|
||
if extra_headers:
|
||
payload["_headers"] = extra_headers
|
||
payload["_raw_body"] = body
|
||
|
||
try:
|
||
result = await handler(payload)
|
||
except Exception:
|
||
logger.exception("Webhook handler failed for channel: %s", channel_type)
|
||
result = None
|
||
finally:
|
||
self._guard.release_concurrency(channel_type)
|
||
|
||
return None, result
|
||
|
||
@property
|
||
def guard(self) -> WebhookGuard:
|
||
return self._guard
|
||
|
||
|
||
webhook_registry = WebhookRegistry()
|
||
|
||
|
||
import re
|
||
|
||
_WEBHOOK_PATH_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
|
||
|
||
|
||
def build_webhook_path(channel_type: str) -> str:
|
||
if not channel_type or not _WEBHOOK_PATH_RE.match(channel_type):
|
||
raise ValueError(f"Invalid channel_type: {channel_type!r}")
|
||
return f"/webhook/{channel_type}"
|
||
|
||
|
||
def build_health_response(status: str, channels: dict, summary: dict, ts: str | None = None) -> dict:
|
||
return {
|
||
"status": status,
|
||
"timestamp": ts,
|
||
"channels": channels,
|
||
"summary": summary,
|
||
}
|