Files
live-hub-py/web/backend/services/proxy_service.py
T
2026-08-31 10:55:44 +08:00

303 lines
10 KiB
Python

"""代理 & 白名单服务层:封装 core/ 代理/白名单逻辑,供路由调用。"""
import asyncio
import threading
import uuid
from sqlalchemy.orm import Session
from core.douyu.proxy import (
parse_proxy_response,
resolve_working_proxy,
verify_proxy_url,
)
from core.douyu.proxy_platforms import create_adapter
from core.douyu.proxy_platforms.base import _get_local_exit_ip
from ..models import AuditLog
from ..models import ProxyConfig as ProxyConfigModel
def _build_whitelist_params(cfg: ProxyConfigModel) -> dict:
"""从 ProxyConfig 构建白名单参数,自动处理新旧字段兼容。
Returns:
{"whitelist_platform": str, "whitelist_credentials": dict|None}
"""
platform = getattr(cfg, "whitelist_platform", None) or "xiequ"
credentials = getattr(cfg, "whitelist_credentials", None)
# 向后兼容:旧字段有值但新字段为空时,自动迁移
if not credentials and cfg.whitelist_uid and cfg.whitelist_ukey:
platform = "xiequ"
credentials = {"uid": cfg.whitelist_uid, "ukey": cfg.whitelist_ukey}
return {
"whitelist_platform": platform,
"whitelist_credentials": credentials if cfg.whitelist_enabled else None,
}
class ProxyService:
"""代理 & 白名单服务:管理代理配置、执行测试。"""
def __init__(self):
# 运行中的测试: test_id -> {log_queue, loop}
self._active_tests: dict[str, dict] = {}
self._active_tests_lock = threading.Lock()
# ---- 配置 CRUD ----
@staticmethod
def get_or_create(db: Session) -> ProxyConfigModel:
"""获取或创建全局代理配置(单条记录)。"""
cfg = db.query(ProxyConfigModel).first()
if not cfg:
cfg = ProxyConfigModel()
db.add(cfg)
db.commit()
db.refresh(cfg)
# 应用层自动迁移:旧字段有值但新字段为空时,填充新字段
if cfg.whitelist_uid and cfg.whitelist_ukey and not cfg.whitelist_credentials:
cfg.whitelist_platform = "xiequ"
cfg.whitelist_credentials = {
"uid": cfg.whitelist_uid,
"ukey": cfg.whitelist_ukey,
}
db.commit()
return cfg
@staticmethod
def update_config(
db: Session,
enabled,
api_url,
http,
https,
whitelist_enabled,
whitelist_platform="xiequ",
whitelist_credentials=None,
whitelist_uid=None,
whitelist_ukey=None,
current_user=None,
) -> ProxyConfigModel:
"""更新代理配置并记录审计日志。"""
cfg = ProxyService.get_or_create(db)
cfg.enabled = enabled
cfg.api_url = api_url
cfg.http = http
cfg.https = https
cfg.whitelist_enabled = whitelist_enabled
cfg.whitelist_platform = whitelist_platform
cfg.whitelist_credentials = whitelist_credentials
# 双写:协固平台同步到旧字段,其他平台清空旧字段
if whitelist_platform == "xiequ" and whitelist_credentials:
cfg.whitelist_uid = whitelist_credentials.get("uid", "")
cfg.whitelist_ukey = whitelist_credentials.get("ukey", "")
else:
# 非协固平台,旧字段使用传入值或清空
cfg.whitelist_uid = whitelist_uid or ""
cfg.whitelist_ukey = whitelist_ukey or ""
db.commit()
db.refresh(cfg)
if current_user:
db.add(
AuditLog(
user_id=current_user.id,
username=current_user.username,
action="proxy:update",
target="proxy_config",
)
)
db.commit()
return cfg
# ---- 测试任务管理 ----
def register_test(self) -> tuple[str, asyncio.Queue, asyncio.AbstractEventLoop]:
"""注册一个新测试任务,返回 (test_id, log_queue, loop)。"""
test_id = uuid.uuid4().hex[:12]
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
with self._active_tests_lock:
self._active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
return test_id, log_queue, loop
def get_test(self, test_id: str) -> dict | None:
"""获取测试任务信息。"""
with self._active_tests_lock:
return self._active_tests.get(test_id)
def remove_test(self, test_id: str) -> None:
"""移除测试任务。"""
with self._active_tests_lock:
self._active_tests.pop(test_id, None)
# ---- 代理测试 ----
def run_proxy_test(
self,
cfg: ProxyConfigModel,
log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop,
):
"""在线程中执行代理测试。"""
def push(level, message):
asyncio.run_coroutine_threadsafe(
log_queue.put({"level": level, "message": message}),
loop,
)
try:
if not cfg.enabled:
push("error", "代理未启用")
push("result", "")
return
# 静态代理
if cfg.http or cfg.https:
proxy_url = cfg.http or cfg.https
push("info", f"验证静态代理: {proxy_url}")
ok, msg = verify_proxy_url(proxy_url)
push("success" if ok else "error", msg)
push("result", "")
return
# API代理
if cfg.api_url:
wl_params = _build_whitelist_params(cfg)
proxy_url, msg = resolve_working_proxy(
api_url=cfg.api_url,
whitelist_platform=wl_params["whitelist_platform"],
whitelist_credentials=wl_params["whitelist_credentials"],
max_attempts=3,
log_func=push,
)
if proxy_url:
push("success", f"代理可用: {proxy_url}")
else:
push("error", msg)
push("result", "")
return
push("error", "未配置代理地址或API")
push("result", "")
except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
push("error", f"测试异常: {e}")
push("result", "")
# ---- 白名单测试 ----
def run_whitelist_test(
self,
cfg: ProxyConfigModel,
log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop,
):
"""在线程中执行白名单测试。"""
import requests as req_lib
def push(level, message):
asyncio.run_coroutine_threadsafe(
log_queue.put({"level": level, "message": message}),
loop,
)
try:
if not cfg.whitelist_enabled:
push("error", "白名单未启用")
push("result", "")
return
# 构建适配器
wl_params = _build_whitelist_params(cfg)
credentials = wl_params["whitelist_credentials"]
platform = wl_params["whitelist_platform"]
if not credentials:
push("error", "未配置白名单凭据")
push("result", "")
return
adapter = create_adapter(platform, credentials)
if not adapter:
push("error", f"不支持的白名单平台: {platform}")
push("result", "")
return
push("info", f"当前白名单平台: {adapter.platform_label}")
# 1. 测试API连接
push("info", "测试白名单API连接...")
ok, msg = adapter.test_connection()
push("info" if ok else "error", f"白名单API: {msg}")
if not ok:
push("result", "")
return
# 2. 获取本机公网IP
push("info", "正在获取本机公网IP...")
local_ip = None
if cfg.api_url:
try:
resp = req_lib.get(cfg.api_url, timeout=10)
text = resp.text.strip()
push("info", f"代理API响应: {text[:80]}")
_, whitelist_ip = parse_proxy_response(text)
if whitelist_ip:
local_ip = whitelist_ip
push("info", f"从代理API获取到本机IP: {local_ip}")
except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
push("warning", f"代理API请求失败: {e}")
if not local_ip:
push("info", "通过IP检测服务获取本机公网IP...")
local_ip = _get_local_exit_ip()
if not local_ip:
push("error", "无法获取本机公网IP")
push("result", "")
return
push("info", f"本机公网IP: {local_ip}")
# 3. 检查并同步白名单
records = adapter.get_whitelist()
in_list = any(r.get("ip") == local_ip for r in records)
if records:
push("info", f"白名单共 {len(records)} 条记录")
if in_list:
record = next((r for r in records if r.get("ip") == local_ip), {})
memo = record.get("memo", "")
if memo == adapter.memo:
push("success", f"本机IP {local_ip} 已在白名单中 (备注正确)")
else:
push(
"warning",
f'本机IP {local_ip} 备注不匹配 (当前: "{memo}"),更新中...',
)
sync_ok, sync_msg = adapter.sync_ip(local_ip)
push("success" if sync_ok else "error", f"白名单更新: {sync_msg}")
else:
push("info", f"正在将 {local_ip} 添加到白名单...")
sync_ok, sync_msg = adapter.sync_ip(local_ip)
push("success" if sync_ok else "error", f"白名单同步: {sync_msg}")
push("result", "")
except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
push("error", f"测试异常: {e}")
push("result", "")
# 模块级单例
proxy_service = ProxyService()