"""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 """ import asyncio import logging logger = logging.getLogger("nexus.ip_allowlist_refresh") REFRESH_INTERVAL = 7200 # 2 hours _last_refresh_at: str = "" _last_subscription_count: int = 0 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(): """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 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 new_sub_ips = parse_subscription_content(resp.text) except Exception as e: logger.error("Subscription fetch failed: %s", e) return # 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 async with AsyncSessionLocal() as session: repo = SettingRepositoryImpl(session) # Subscription IPs — replaced entirely await repo.set("login_subscription_ips", sub_val) # Combined — for auth check await repo.set("login_allowed_ips", combined_val) settings.LOGIN_SUBSCRIPTION_IPS = sub_val settings.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), ) except Exception as e: logger.error("Failed to save subscription IPs: %s", e) def get_last_refresh_time() -> str: return _last_refresh_at def get_subscription_count() -> int: return _last_subscription_count