From 3ee5275ea0c826197accd528a89534aff8c43f36 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 11 May 2026 13:03:29 +0300 Subject: [PATCH] backend: PROXY_STATIC_LIST support (manual proxies without API token) - proxy_pool now loads from both PROXY_STATIC_LIST (env, comma-separated) and PROXY6_TOKEN (API) - Static list has priority, merged with API list (dedup by URL) - /api/proxy_status returns masked proxy URLs for diagnostic (passwords hidden) - Supports formats: 'http://user:pass@host:port' or 'host:port' (assumed http://) --- backend-py/app/config.py | 2 + backend-py/app/proxy_pool.py | 103 +++++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/backend-py/app/config.py b/backend-py/app/config.py index 8238168..2de714d 100644 --- a/backend-py/app/config.py +++ b/backend-py/app/config.py @@ -20,6 +20,7 @@ class Config: grace_period_days: int proxy6_token: str # пусто = без прокси (прямой HTTP) + proxy_static_list: str # статический список прокси через запятую: "http://user:pass@host:port,..." def _required(name: str) -> str: @@ -42,4 +43,5 @@ def get_config() -> Config: active_period_days=int(os.getenv("ACTIVE_PERIOD_DAYS", "90")), grace_period_days=int(os.getenv("GRACE_PERIOD_DAYS", "14")), proxy6_token=os.getenv("PROXY6_TOKEN", ""), + proxy_static_list=os.getenv("PROXY_STATIC_LIST", ""), ) diff --git a/backend-py/app/proxy_pool.py b/backend-py/app/proxy_pool.py index 5db2a57..7a7d2c4 100644 --- a/backend-py/app/proxy_pool.py +++ b/backend-py/app/proxy_pool.py @@ -29,50 +29,74 @@ _pool: list[str] = [] # ["http://user:pass@host:port", ...] _pool_loaded_at: float = 0.0 +def _parse_static_list(raw: str) -> list[str]: + """Парсит PROXY_STATIC_LIST — строка с прокси через запятую/перевод строки.""" + if not raw: + return [] + parts = [p.strip() for p in raw.replace("\n", ",").split(",")] + proxies = [] + for p in parts: + if not p: + continue + # Если протокол не указан — добавляем http:// + if "://" not in p: + p = "http://" + p + proxies.append(p) + return proxies + + def _load_pool(force: bool = False) -> list[str]: - """Загружает активные прокси из Proxy6 API. Кэшируется на _POOL_TTL_SEC.""" + """Загружает прокси: сначала статический список из ENV, потом дополняет из Proxy6 API. + Кэшируется на _POOL_TTL_SEC.""" global _pool, _pool_loaded_at with _lock: now = time.time() if not force and _pool and now - _pool_loaded_at < _POOL_TTL_SEC: return _pool - token = get_config().proxy6_token - if not token: - return _pool # без токена — пустой пул, парсеры пойдут напрямую - - try: - with httpx.Client(timeout=10.0) as client: - r = client.get(f"{_API_URL}/{token}/getproxy", params={"state": "active"}) - data = r.json() - except Exception as e: - log.warning("Proxy6 API request failed: %s", e) - return _pool - - if data.get("status") != "yes": - log.warning("Proxy6 returned status=%s, error=%s", - data.get("status"), data.get("error")) - return _pool - + cfg = get_config() proxies: list[str] = [] - for _, p in (data.get("list") or {}).items(): - if str(p.get("active")) != "1": - continue - proto = (p.get("type") or "http").lower() - # Proxy6 возвращает 'socks' для SOCKS5 - if proto == "socks": - proto = "socks5" - host = p.get("host") or p.get("ip") - port = p.get("port") - user = p.get("user") - pwd = p.get("pass") - if not (host and port and user and pwd): - continue - proxies.append(f"{proto}://{user}:{pwd}@{host}:{port}") + + # 1) Статический список из ENV (приоритет, для одиночных IP без API) + static = _parse_static_list(cfg.proxy_static_list) + if static: + proxies.extend(static) + log.info("Static proxy list: %d entries", len(static)) + + # 2) Динамический пул из Proxy6 API (если есть токен) + if cfg.proxy6_token: + try: + with httpx.Client(timeout=10.0) as client: + r = client.get(f"{_API_URL}/{cfg.proxy6_token}/getproxy", + params={"state": "active"}) + data = r.json() + if data.get("status") == "yes": + for _, p in (data.get("list") or {}).items(): + if str(p.get("active")) != "1": + continue + proto = (p.get("type") or "http").lower() + if proto == "socks": + proto = "socks5" + host = p.get("host") or p.get("ip") + port = p.get("port") + user = p.get("user") + pwd = p.get("pass") + if not (host and port and user and pwd): + continue + url = f"{proto}://{user}:{pwd}@{host}:{port}" + if url not in proxies: + proxies.append(url) + log.info("Proxy6 API: total pool now %d proxies", len(proxies)) + else: + log.warning("Proxy6 API returned status=%s error=%s", + data.get("status"), data.get("error")) + except Exception as e: + log.warning("Proxy6 API request failed: %s", e) _pool = proxies _pool_loaded_at = now - log.info("Proxy6 pool loaded: %d active proxies", len(_pool)) + if not _pool: + log.info("Proxy pool is empty — parsers will use direct HTTP") return _pool @@ -95,8 +119,19 @@ def proxied_client(timeout: float = 15.0, **client_kwargs) -> httpx.Client: def pool_status() -> dict: """Для диагностики — текущее состояние пула.""" pool = _load_pool() + cfg = get_config() + # Маскируем пароли в URL для diagnostic + masked = [] + for p in pool: + try: + import re as _re + masked.append(_re.sub(r"://([^:]+):([^@]+)@", r"://\1:***@", p)) + except Exception: + masked.append("***") return { "count": len(pool), "loaded_age_sec": int(time.time() - _pool_loaded_at) if _pool_loaded_at else None, - "token_configured": bool(get_config().proxy6_token), + "token_configured": bool(cfg.proxy6_token), + "static_list_size": len(_parse_static_list(cfg.proxy_static_list)), + "proxies": masked, }