704764bd7e
保存 IP 白名单时同步刷新并 Redis 广播 login_* 设置;推送/文件上传与 Nginx 模板对齐为 500MB,避免生产 50m/100m 导致大文件失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
"""Background task: refresh login subscription IPs every 2 hours.
|
|
|
|
Data model:
|
|
login_subscription_url — URL to fetch (configurable in settings)
|
|
login_subscription_ips — IPs from last fetch (completely replaced each time)
|
|
login_manual_ips — manually added (never touched by auto-refresh)
|
|
login_allowed_ips — combined: subscription + manual (used by auth check)
|
|
login_allowlist_enabled — "true" / "false" master switch
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger("nexus.ip_allowlist_refresh")
|
|
|
|
REFRESH_INTERVAL = 7200 # 2 hours
|
|
_last_refresh_at: str = ""
|
|
_last_subscription_count: int = 0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RefreshResult:
|
|
ok: bool
|
|
subscription_ips: tuple[str, ...] = ()
|
|
last_refresh: str = ""
|
|
subscription_count: int = 0
|
|
error: str | None = None
|
|
|
|
|
|
async def ip_allowlist_refresh_loop():
|
|
"""Every 2 hours, re-fetch subscription IPs and rebuild combined allowlist."""
|
|
logger.info("IP allowlist refresh loop started (interval=%sh)", REFRESH_INTERVAL // 3600)
|
|
# Initial refresh shortly after startup
|
|
await asyncio.sleep(5)
|
|
await do_refresh()
|
|
while True:
|
|
await asyncio.sleep(REFRESH_INTERVAL)
|
|
await do_refresh()
|
|
|
|
|
|
async def do_refresh() -> RefreshResult:
|
|
"""Fetch subscription, replace subscription IPs, rebuild combined list."""
|
|
return await _do_refresh()
|
|
|
|
|
|
async def _do_refresh() -> RefreshResult:
|
|
"""Fetch subscription, replace subscription IPs, rebuild combined list."""
|
|
global _last_refresh_at, _last_subscription_count
|
|
from server.config import settings
|
|
from server.infrastructure.subscription_parser import parse_subscription_content
|
|
|
|
sub_url = (settings.LOGIN_SUBSCRIPTION_URL or "").strip()
|
|
if not sub_url:
|
|
return RefreshResult(ok=True)
|
|
|
|
logger.info("Refreshing subscription IPs from: %s", sub_url[:80])
|
|
try:
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
resp = await client.get(sub_url, follow_redirects=True)
|
|
if resp.status_code != 200:
|
|
logger.warning("Subscription returned HTTP %s", resp.status_code)
|
|
return RefreshResult(ok=False, error=f"订阅链接返回 HTTP {resp.status_code}")
|
|
new_sub_ips = parse_subscription_content(resp.text)
|
|
except Exception as e:
|
|
logger.error("Subscription fetch failed: %s", e)
|
|
return RefreshResult(ok=False, error=f"拉取订阅失败: {e}")
|
|
|
|
# Manual IPs (always preserved)
|
|
manual_raw = (settings.LOGIN_MANUAL_IPS or "").strip()
|
|
manual_ips = [ip.strip() for ip in manual_raw.split(",") if ip.strip()]
|
|
|
|
# Combined list: subscription first, then manual (dedup)
|
|
seen: set[str] = set()
|
|
combined: list[str] = []
|
|
for ip in new_sub_ips + manual_ips:
|
|
if ip not in seen:
|
|
seen.add(ip)
|
|
combined.append(ip)
|
|
|
|
sub_val = ",".join(new_sub_ips)
|
|
combined_val = ",".join(combined)
|
|
|
|
try:
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
|
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
|
|
from server.infrastructure.settings_broadcast import publish_setting_changes
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
repo = SettingRepositoryImpl(session)
|
|
await repo.set("login_subscription_ips", sub_val)
|
|
await repo.set("login_allowed_ips", combined_val)
|
|
|
|
settings.LOGIN_SUBSCRIPTION_IPS = sub_val
|
|
settings.LOGIN_ALLOWED_IPS = combined_val
|
|
|
|
await publish_setting_changes({
|
|
"login_subscription_ips": sub_val,
|
|
"login_allowed_ips": combined_val,
|
|
})
|
|
|
|
from server.utils.display_time import format_beijing
|
|
|
|
_last_refresh_at = format_beijing(with_label=False)
|
|
_last_subscription_count = len(new_sub_ips)
|
|
logger.info(
|
|
"Subscription IPs refreshed: %d → combined %d (+ %d manual)",
|
|
len(new_sub_ips), len(combined), len(manual_ips),
|
|
)
|
|
return RefreshResult(
|
|
ok=True,
|
|
subscription_ips=tuple(new_sub_ips),
|
|
last_refresh=_last_refresh_at,
|
|
subscription_count=len(new_sub_ips),
|
|
)
|
|
except Exception as e:
|
|
logger.error("Failed to save subscription IPs: %s", e)
|
|
return RefreshResult(ok=False, error=f"保存订阅 IP 失败: {e}")
|
|
|
|
|
|
def get_last_refresh_time() -> str:
|
|
return _last_refresh_at
|
|
|
|
|
|
def get_subscription_count() -> int:
|
|
return _last_subscription_count
|