"""代理 & 白名单服务层:封装 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()