92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
|
|
"""Background task: refresh login IP allowlist from subscription URL every 2 hours.
|
||
|
|
|
||
|
|
Flow:
|
||
|
|
1. Read LOGIN_SUBSCRIPTION_URL from settings
|
||
|
|
2. Fetch & parse proxy nodes → subscription IPs
|
||
|
|
3. Merge with LOGIN_MANUAL_IPS (manually added, always preserved)
|
||
|
|
4. Write combined list to LOGIN_ALLOWED_IPS + settings table
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import logging
|
||
|
|
|
||
|
|
logger = logging.getLogger("nexus.ip_allowlist_refresh")
|
||
|
|
|
||
|
|
REFRESH_INTERVAL = 7200 # 2 hours
|
||
|
|
_last_refresh_at: str = ""
|
||
|
|
|
||
|
|
|
||
|
|
async def ip_allowlist_refresh_loop():
|
||
|
|
"""Every 2 hours, re-fetch subscription and rebuild IP allowlist."""
|
||
|
|
global _last_refresh_at
|
||
|
|
logger.info("IP allowlist refresh loop started (interval=%sh)", REFRESH_INTERVAL // 3600)
|
||
|
|
|
||
|
|
# Do one refresh on startup if subscription URL is configured
|
||
|
|
await asyncio.sleep(5)
|
||
|
|
await _do_refresh()
|
||
|
|
|
||
|
|
while True:
|
||
|
|
await asyncio.sleep(REFRESH_INTERVAL)
|
||
|
|
await _do_refresh()
|
||
|
|
|
||
|
|
|
||
|
|
async def _do_refresh():
|
||
|
|
"""Fetch subscription and update allowlist. Non-fatal on error."""
|
||
|
|
global _last_refresh_at
|
||
|
|
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 # No subscription configured — nothing to do
|
||
|
|
|
||
|
|
logger.info("Refreshing IP allowlist from subscription: %s", sub_url[:60])
|
||
|
|
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 URL returned HTTP %s", resp.status_code)
|
||
|
|
return
|
||
|
|
sub_ips = parse_subscription_content(resp.text)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error("Failed to refresh IP allowlist from subscription: %s", e)
|
||
|
|
return
|
||
|
|
|
||
|
|
# Load manual IPs (preserved)
|
||
|
|
manual_raw = (settings.LOGIN_MANUAL_IPS or "").strip()
|
||
|
|
manual_ips = [ip.strip() for ip in manual_raw.split(",") if ip.strip()]
|
||
|
|
|
||
|
|
# Merge: subscription first, then manual (dedup)
|
||
|
|
seen: set[str] = set()
|
||
|
|
merged: list[str] = []
|
||
|
|
for ip in sub_ips + manual_ips:
|
||
|
|
if ip not in seen:
|
||
|
|
seen.add(ip)
|
||
|
|
merged.append(ip)
|
||
|
|
|
||
|
|
combined = ",".join(merged)
|
||
|
|
|
||
|
|
# Write to settings table + update runtime config
|
||
|
|
try:
|
||
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
||
|
|
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
|
||
|
|
async with AsyncSessionLocal() as session:
|
||
|
|
repo = SettingRepositoryImpl(session)
|
||
|
|
await repo.set("login_allowed_ips", combined)
|
||
|
|
|
||
|
|
settings.LOGIN_ALLOWED_IPS = combined
|
||
|
|
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
_last_refresh_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||
|
|
logger.info(
|
||
|
|
"IP allowlist refreshed: %d subscription + %d manual = %d total",
|
||
|
|
len(sub_ips), len(manual_ips), len(merged),
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error("Failed to save refreshed IP allowlist: %s", e)
|
||
|
|
|
||
|
|
|
||
|
|
def get_last_refresh_time() -> str:
|
||
|
|
return _last_refresh_at
|