Files
gitlab-instance-0a899031_d2…/web2_network_routes.py
2026-07-10 05:15:29 -05:00

211 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import asyncio
import time
from typing import Any, Awaitable, Callable, Optional
from fastapi import APIRouter, Query
from fastapi.responses import JSONResponse
from web2_ip_profile import run_ip_profile
from web2_ip_quality import run_ip_quality
from web2_network import run_network_dashboard, run_network_toolkit
router = APIRouter()
_PROFILE_TTL_SECONDS = 300.0
_QUALITY_TTL_SECONDS = 600.0
_NETWORK_TTL_SECONDS = 300.0
_NETWORK_TOOLKIT_TTL_SECONDS = 300.0
_cache_lock = asyncio.Lock()
_profile_cache: dict[str, tuple[float, dict[str, Any]]] = {}
_quality_cache: tuple[float, dict[str, Any]] | None = None
_network_cache: tuple[float, dict[str, Any]] | None = None
_network_toolkit_cache: tuple[float, dict[str, Any]] | None = None
_inflight: dict[str, asyncio.Task[dict[str, Any]]] = {}
def _profile_cache_key(tz: str, lang: str, leak_flag: Optional[bool]) -> str:
leak = "none" if leak_flag is None else ("1" if leak_flag else "0")
return f"{tz}|{lang}|{leak}"
async def _cached_fetch(
key: str,
*,
ttl: float,
force_refresh: bool,
fetcher: Callable[[], Awaitable[dict[str, Any]]],
store: Callable[[str, float, dict[str, Any]], None],
load: Callable[[str], Optional[tuple[float, dict[str, Any]]]],
) -> dict[str, Any]:
now = time.time()
if not force_refresh:
cached = load(key)
if cached and (now - cached[0]) < ttl:
return cached[1]
async with _cache_lock:
if not force_refresh:
cached = load(key)
if cached and (time.time() - cached[0]) < ttl:
return cached[1]
inflight = _inflight.get(key)
if inflight is not None:
return await asyncio.shield(inflight)
task = asyncio.create_task(fetcher())
_inflight[key] = task
try:
payload = await task
finally:
async with _cache_lock:
if _inflight.get(key) is task:
_inflight.pop(key, None)
store(key, time.time(), payload)
return payload
def _store_profile(key: str, at: float, payload: dict[str, Any]) -> None:
_profile_cache[key] = (at, payload)
def _load_profile(key: str) -> Optional[tuple[float, dict[str, Any]]]:
return _profile_cache.get(key)
def _store_quality(key: str, at: float, payload: dict[str, Any]) -> None:
global _quality_cache
_quality_cache = (at, payload)
def _load_quality(_key: str) -> Optional[tuple[float, dict[str, Any]]]:
return _quality_cache
def _store_network(_key: str, at: float, payload: dict[str, Any]) -> None:
global _network_cache
_network_cache = (at, payload)
def _load_network(_key: str) -> Optional[tuple[float, dict[str, Any]]]:
return _network_cache
def _store_network_toolkit(_key: str, at: float, payload: dict[str, Any]) -> None:
global _network_toolkit_cache
_network_toolkit_cache = (at, payload)
def _load_network_toolkit(_key: str) -> Optional[tuple[float, dict[str, Any]]]:
return _network_toolkit_cache
async def _get_ip_profile_cached(
*,
tz: str,
lang: str,
leak_flag: Optional[bool],
force_refresh: bool,
) -> dict[str, Any]:
cache_key = _profile_cache_key(tz, lang, leak_flag)
return await _cached_fetch(
cache_key,
ttl=_PROFILE_TTL_SECONDS,
force_refresh=force_refresh,
fetcher=lambda: run_ip_profile(client_tz=tz, client_lang=lang, webrtc_leak=leak_flag),
store=_store_profile,
load=_load_profile,
)
async def _get_ip_quality_cached(*, force_refresh: bool) -> dict[str, Any]:
return await _cached_fetch(
"quality",
ttl=_QUALITY_TTL_SECONDS,
force_refresh=force_refresh,
fetcher=run_ip_quality,
store=_store_quality,
load=_load_quality,
)
async def _get_network_status_cached(*, force_refresh: bool) -> dict[str, Any]:
return await _cached_fetch(
"network_v2",
ttl=_NETWORK_TTL_SECONDS,
force_refresh=force_refresh,
fetcher=run_network_dashboard,
store=_store_network,
load=_load_network,
)
async def _get_network_toolkit_cached(*, force_refresh: bool) -> dict[str, Any]:
return await _cached_fetch(
"network_toolkit_v2",
ttl=_NETWORK_TOOLKIT_TTL_SECONDS,
force_refresh=force_refresh,
fetcher=run_network_toolkit,
store=_store_network_toolkit,
load=_load_network_toolkit,
)
@router.get("/network_status")
async def network_status(
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
):
"""Google / 抖音 连通与简易网速(供仪表盘)。"""
try:
return await _get_network_status_cached(force_refresh=force_refresh)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@router.get("/network_toolkit")
async def network_toolkit(
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
):
"""网络增强工具聚合speedtest、mtr、ipinfo、system 信息等。"""
try:
return await _get_network_toolkit_cached(force_refresh=force_refresh)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@router.get("/ip_profile")
async def ip_profile(
tz: str = Query("", description="客户端 IANA 时区,如 Asia/Shanghai"),
lang: str = Query("", description="客户端语言,如 zh-CN"),
webrtc_leak: Optional[str] = Query(None, description="1/true 表示检测到 WebRTC 本地泄露"),
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
):
"""静态住宅 IP / 出口画像(参考 whoer、ping0.cc"""
leak_flag: Optional[bool] = None
if webrtc_leak is not None:
leak_flag = str(webrtc_leak).strip().lower() in ("1", "true", "yes")
try:
return await _get_ip_profile_cached(
tz=tz.strip(),
lang=lang.strip(),
leak_flag=leak_flag,
force_refresh=force_refresh,
)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@router.get("/ip_quality")
async def ip_quality(
force_refresh: bool = Query(False, description="是否跳过缓存强制重新检测"),
):
"""IP 质量检测(集成 xykt/IPQuality 数据源与流媒体解锁)。"""
try:
return await _get_ip_quality_cached(force_refresh=force_refresh)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)