Initial project import
This commit is contained in:
@@ -0,0 +1,648 @@
|
||||
"""Global outbound proxy and Cloudflare clearance helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field, replace
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Mapping
|
||||
from urllib import request as urllib_request
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from curl_cffi.requests import Session
|
||||
|
||||
from services.config import config
|
||||
|
||||
|
||||
FlareSolverrRequestMethod = Callable[[str, bytes, dict[str, str], float], bytes]
|
||||
|
||||
|
||||
def normalize_proxy_url(url: str) -> str:
|
||||
"""Normalize proxy URLs for curl_cffi.
|
||||
|
||||
SOCKS proxies should use remote-DNS resolution by default, so generic
|
||||
``socks://`` and ``socks5://`` inputs are upgraded to ``socks5h://``.
|
||||
HTTP/HTTPS/socks5h inputs are otherwise left untouched except trimming.
|
||||
"""
|
||||
candidate = str(url or "").strip()
|
||||
if candidate and "://" not in candidate:
|
||||
candidate = _colon_proxy_to_url(candidate)
|
||||
lowered = candidate.lower()
|
||||
if lowered.startswith("socks://"):
|
||||
return "socks5h://" + candidate[len("socks://") :]
|
||||
if lowered.startswith("socks5://"):
|
||||
return "socks5h://" + candidate[len("socks5://") :]
|
||||
return candidate
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProxyRuntimeProfile:
|
||||
proxy_url: str = ""
|
||||
proxy_source: str = "direct"
|
||||
resource: bool = False
|
||||
runtime_enabled: bool = False
|
||||
egress_mode: str = "direct"
|
||||
skip_ssl_verify: bool = False
|
||||
reset_session_status_codes: tuple[int, ...] = field(default_factory=lambda: (403,))
|
||||
clearance: dict[str, object] = field(default_factory=dict, repr=False)
|
||||
|
||||
@property
|
||||
def clearance_enabled(self) -> bool:
|
||||
return (
|
||||
self.runtime_enabled
|
||||
and bool(self.clearance.get("enabled"))
|
||||
and self.clearance_mode in {"manual", "flaresolverr"}
|
||||
)
|
||||
|
||||
@property
|
||||
def clearance_mode(self) -> str:
|
||||
return str(self.clearance.get("mode") or "none").strip().lower()
|
||||
|
||||
@property
|
||||
def refresh_interval(self) -> int:
|
||||
try:
|
||||
return max(0, int(self.clearance.get("refresh_interval") or 0))
|
||||
except (OverflowError, TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
@property
|
||||
def timeout_sec(self) -> int:
|
||||
try:
|
||||
return max(1, int(self.clearance.get("timeout_sec") or 60))
|
||||
except (OverflowError, TypeError, ValueError):
|
||||
return 60
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClearanceBundle:
|
||||
target_host: str
|
||||
proxy_url: str = ""
|
||||
cookies: dict[str, str] = field(default_factory=dict, repr=False)
|
||||
user_agent: str = ""
|
||||
created_at: float = field(default_factory=time.time)
|
||||
expires_at: float | None = None
|
||||
|
||||
def is_valid_for(self, target_host: str, proxy_url: str, *, now: float | None = None) -> bool:
|
||||
host = _normalize_host(target_host)
|
||||
if self.target_host and host and _normalize_host(self.target_host) != host:
|
||||
return False
|
||||
if normalize_proxy_url(self.proxy_url) != normalize_proxy_url(proxy_url):
|
||||
return False
|
||||
if self.expires_at is not None and (time.time() if now is None else now) >= self.expires_at:
|
||||
return False
|
||||
return bool(self.cookies or self.user_agent)
|
||||
|
||||
def cookie_header(self) -> str:
|
||||
return _cookies_to_header(self.cookies)
|
||||
|
||||
|
||||
class FlareSolverrClearanceProvider:
|
||||
def __init__(self, flaresolverr_url: str, request_method: FlareSolverrRequestMethod | None = None) -> None:
|
||||
self.flaresolverr_url = str(flaresolverr_url or "").strip().rstrip("/")
|
||||
self._request_method = request_method or self._urllib_post
|
||||
|
||||
def get_clearance(self, target_url: str, proxy_url: str = "", timeout_sec: int = 60) -> ClearanceBundle | None:
|
||||
if not self.flaresolverr_url:
|
||||
return None
|
||||
|
||||
timeout = _coerce_timeout(timeout_sec)
|
||||
payload: dict[str, object] = {
|
||||
"cmd": "request.get",
|
||||
"url": str(target_url or ""),
|
||||
"maxTimeout": int(timeout * 1000),
|
||||
}
|
||||
proxy_url = normalize_proxy_url(proxy_url)
|
||||
if proxy_url:
|
||||
payload["proxy"] = {"url": proxy_url}
|
||||
|
||||
endpoint = f"{self.flaresolverr_url}/v1"
|
||||
try:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
raw_response = self._request_method(
|
||||
endpoint,
|
||||
body,
|
||||
{"Content-Type": "application/json"},
|
||||
timeout,
|
||||
)
|
||||
data = json.loads(raw_response.decode("utf-8") if isinstance(raw_response, bytes) else raw_response)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not isinstance(data, dict) or str(data.get("status") or "").lower() != "ok":
|
||||
return None
|
||||
solution = data.get("solution")
|
||||
if not isinstance(solution, dict):
|
||||
return None
|
||||
|
||||
target_host = _host_from_url(target_url)
|
||||
cookies = _filter_flaresolverr_cookies(solution.get("cookies"), target_host)
|
||||
user_agent = str(solution.get("userAgent") or "").strip()
|
||||
if not cookies and not user_agent:
|
||||
return None
|
||||
return ClearanceBundle(
|
||||
target_host=target_host,
|
||||
proxy_url=proxy_url,
|
||||
cookies=cookies,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _urllib_post(endpoint: str, body: bytes, headers: dict[str, str], timeout: float) -> bytes:
|
||||
req = urllib_request.Request(endpoint, data=body, headers=headers, method="POST")
|
||||
with urllib_request.urlopen(req, timeout=timeout) as response:
|
||||
return response.read()
|
||||
|
||||
|
||||
class ProxySettingsStore:
|
||||
def __init__(
|
||||
self,
|
||||
config_store=None,
|
||||
clearance_provider_factory: Callable[[str], FlareSolverrClearanceProvider] | None = None,
|
||||
) -> None:
|
||||
self._config = config_store or config
|
||||
self._clearance_provider_factory = clearance_provider_factory or FlareSolverrClearanceProvider
|
||||
self._clearance_cache: dict[tuple[str, str], ClearanceBundle] = {}
|
||||
self._provider_cache: dict[str, FlareSolverrClearanceProvider] = {}
|
||||
self._flight_locks: dict[tuple[str, str], threading.Lock] = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def get_profile(
|
||||
self,
|
||||
account: dict | None = None,
|
||||
proxy: str = "",
|
||||
resource: bool = False,
|
||||
upstream: bool = False,
|
||||
) -> ProxyRuntimeProfile:
|
||||
runtime = self._get_runtime_settings()
|
||||
clearance = dict(runtime.get("clearance") if isinstance(runtime.get("clearance"), dict) else {})
|
||||
runtime_enabled = bool(runtime.get("enabled"))
|
||||
egress_mode = str(runtime.get("egress_mode") or "direct").strip().lower()
|
||||
|
||||
account_proxy = _clean((account or {}).get("proxy") if isinstance(account, dict) else "")
|
||||
explicit_proxy = _clean(proxy)
|
||||
legacy_proxy = _clean(self._config.get_proxy_settings())
|
||||
|
||||
runtime_proxy = ""
|
||||
runtime_proxy_source = "runtime"
|
||||
if upstream and runtime_enabled and egress_mode == "single_proxy":
|
||||
resource_proxy = _clean(runtime.get("resource_proxy_url")) if resource else ""
|
||||
runtime_proxy = resource_proxy or _clean(runtime.get("proxy_url"))
|
||||
runtime_proxy_source = "runtime_resource" if resource_proxy else "runtime"
|
||||
|
||||
selected_proxy = ""
|
||||
source = "direct"
|
||||
if account_proxy:
|
||||
selected_proxy = account_proxy
|
||||
source = "account"
|
||||
elif runtime_proxy:
|
||||
selected_proxy = runtime_proxy
|
||||
source = runtime_proxy_source
|
||||
elif explicit_proxy:
|
||||
selected_proxy = explicit_proxy
|
||||
source = "explicit"
|
||||
elif legacy_proxy:
|
||||
selected_proxy = legacy_proxy
|
||||
source = "global"
|
||||
|
||||
return ProxyRuntimeProfile(
|
||||
proxy_url=normalize_proxy_url(selected_proxy),
|
||||
proxy_source=source,
|
||||
resource=bool(resource),
|
||||
runtime_enabled=runtime_enabled,
|
||||
egress_mode=egress_mode,
|
||||
skip_ssl_verify=bool(runtime.get("skip_ssl_verify")) if runtime_enabled else False,
|
||||
reset_session_status_codes=_status_codes_tuple(runtime.get("reset_session_status_codes")),
|
||||
clearance=clearance,
|
||||
)
|
||||
|
||||
def build_session_kwargs(
|
||||
self,
|
||||
account: dict | None = None,
|
||||
proxy: str = "",
|
||||
resource: bool = False,
|
||||
upstream: bool = False,
|
||||
**session_kwargs,
|
||||
) -> dict[str, object]:
|
||||
profile = self.get_profile(account=account, proxy=proxy, resource=resource, upstream=upstream)
|
||||
if profile.proxy_url:
|
||||
session_kwargs["proxy"] = profile.proxy_url
|
||||
if profile.runtime_enabled and profile.skip_ssl_verify:
|
||||
session_kwargs["verify"] = False
|
||||
return session_kwargs
|
||||
|
||||
def build_headers(
|
||||
self,
|
||||
headers: Mapping[str, object] | None = None,
|
||||
target_url: str = "https://chatgpt.com",
|
||||
account: dict | None = None,
|
||||
proxy: str = "",
|
||||
resource: bool = False,
|
||||
upstream: bool = True,
|
||||
) -> dict[str, object]:
|
||||
merged_headers: dict[str, object] = dict(headers or {})
|
||||
profile = self.get_profile(account=account, proxy=proxy, resource=resource, upstream=upstream)
|
||||
if not profile.clearance_enabled:
|
||||
return merged_headers
|
||||
|
||||
target_host = _host_from_url(target_url)
|
||||
bundle = self._bundle_for_headers(profile, target_host)
|
||||
if bundle is None or not bundle.is_valid_for(target_host, profile.proxy_url):
|
||||
return merged_headers
|
||||
|
||||
if bundle.user_agent and _find_header_key(merged_headers, "user-agent") is None:
|
||||
merged_headers["User-Agent"] = bundle.user_agent
|
||||
|
||||
if bundle.cookies:
|
||||
cookie_key = _find_header_key(merged_headers, "cookie") or "Cookie"
|
||||
existing_cookie = str(merged_headers.get(cookie_key) or "")
|
||||
cookie_header = _merge_cookie_header(existing_cookie, bundle.cookies)
|
||||
if cookie_header:
|
||||
merged_headers[cookie_key] = cookie_header
|
||||
return merged_headers
|
||||
|
||||
def refresh_clearance(
|
||||
self,
|
||||
target_url: str = "https://chatgpt.com",
|
||||
account: dict | None = None,
|
||||
proxy: str = "",
|
||||
resource: bool = False,
|
||||
force: bool = False,
|
||||
upstream: bool = True,
|
||||
) -> ClearanceBundle | None:
|
||||
profile = self.get_profile(account=account, proxy=proxy, resource=resource, upstream=upstream)
|
||||
if not profile.clearance_enabled:
|
||||
return None
|
||||
|
||||
target_host = _host_from_url(target_url)
|
||||
key = self._cache_key(profile.proxy_url, target_host)
|
||||
if profile.clearance_mode == "manual":
|
||||
bundle = self._build_manual_bundle(profile, target_host)
|
||||
if bundle is not None:
|
||||
self._set_cached_bundle(key, bundle)
|
||||
return bundle
|
||||
if profile.clearance_mode != "flaresolverr":
|
||||
return None
|
||||
|
||||
cached_before = self._get_cached_bundle(key)
|
||||
if cached_before is not None and not force and cached_before.is_valid_for(target_host, profile.proxy_url):
|
||||
return cached_before
|
||||
|
||||
lock = self._get_flight_lock(key)
|
||||
if not lock.acquire(blocking=False):
|
||||
with lock:
|
||||
pass
|
||||
return self._get_cached_bundle(key) or cached_before
|
||||
|
||||
try:
|
||||
cached_now = self._get_cached_bundle(key)
|
||||
if cached_now is not None and not force and cached_now.is_valid_for(target_host, profile.proxy_url):
|
||||
return cached_now
|
||||
|
||||
flaresolverr_url = str(profile.clearance.get("flaresolverr_url") or "").strip()
|
||||
provider = self._get_provider(flaresolverr_url)
|
||||
new_bundle = provider.get_clearance(target_url, proxy_url=profile.proxy_url, timeout_sec=profile.timeout_sec)
|
||||
if new_bundle is not None:
|
||||
expires_at = time.time() + profile.refresh_interval if profile.refresh_interval else None
|
||||
if (
|
||||
not new_bundle.target_host
|
||||
or normalize_proxy_url(new_bundle.proxy_url) != normalize_proxy_url(profile.proxy_url)
|
||||
or new_bundle.expires_at != expires_at
|
||||
):
|
||||
new_bundle = replace(
|
||||
new_bundle,
|
||||
target_host=new_bundle.target_host or target_host,
|
||||
proxy_url=profile.proxy_url,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
self._set_cached_bundle(key, new_bundle)
|
||||
return new_bundle
|
||||
return cached_now or cached_before
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def invalidate_clearance(
|
||||
self,
|
||||
target_url: str = "https://chatgpt.com",
|
||||
account: dict | None = None,
|
||||
proxy: str = "",
|
||||
resource: bool = False,
|
||||
upstream: bool = True,
|
||||
) -> None:
|
||||
profile = self.get_profile(account=account, proxy=proxy, resource=resource, upstream=upstream)
|
||||
target_host = _host_from_url(target_url)
|
||||
key = self._cache_key(profile.proxy_url, target_host)
|
||||
with self._lock:
|
||||
self._clearance_cache.pop(key, None)
|
||||
|
||||
def get_runtime_status(self) -> dict[str, object]:
|
||||
profile = self.get_profile(upstream=True)
|
||||
with self._lock:
|
||||
cached_hosts = [host for _proxy, host in self._clearance_cache]
|
||||
cached_count = len(self._clearance_cache)
|
||||
return {
|
||||
"enabled": profile.runtime_enabled,
|
||||
"egress_mode": profile.egress_mode,
|
||||
"proxy_source": profile.proxy_source,
|
||||
"has_proxy": bool(profile.proxy_url),
|
||||
"clearance_enabled": profile.clearance_enabled,
|
||||
"clearance_mode": profile.clearance_mode,
|
||||
"has_clearance_bundle": cached_count > 0,
|
||||
"cached_clearance_hosts": sorted(set(cached_hosts)),
|
||||
}
|
||||
|
||||
def _get_runtime_settings(self) -> dict[str, object]:
|
||||
try:
|
||||
runtime = self._config.get_proxy_runtime_settings()
|
||||
except AttributeError:
|
||||
runtime = {}
|
||||
return runtime if isinstance(runtime, dict) else {}
|
||||
|
||||
def _bundle_for_headers(self, profile: ProxyRuntimeProfile, target_host: str) -> ClearanceBundle | None:
|
||||
key = self._cache_key(profile.proxy_url, target_host)
|
||||
if profile.clearance_mode == "manual":
|
||||
bundle = self._build_manual_bundle(profile, target_host)
|
||||
if bundle is not None:
|
||||
self._set_cached_bundle(key, bundle)
|
||||
return bundle
|
||||
if profile.clearance_mode == "flaresolverr":
|
||||
return self._get_cached_bundle(key)
|
||||
return None
|
||||
|
||||
def _build_manual_bundle(self, profile: ProxyRuntimeProfile, target_host: str) -> ClearanceBundle | None:
|
||||
cookies = _parse_cookie_header(str(profile.clearance.get("cf_cookies") or ""))
|
||||
cf_clearance = str(profile.clearance.get("cf_clearance") or "").strip()
|
||||
if cf_clearance and "cf_clearance" not in cookies:
|
||||
cookies["cf_clearance"] = cf_clearance
|
||||
user_agent = str(profile.clearance.get("user_agent") or "").strip()
|
||||
if not cookies and not user_agent:
|
||||
return None
|
||||
|
||||
now = time.time()
|
||||
expires_at = now + profile.refresh_interval if profile.refresh_interval else None
|
||||
return ClearanceBundle(
|
||||
target_host=target_host,
|
||||
proxy_url=profile.proxy_url,
|
||||
cookies=cookies,
|
||||
user_agent=user_agent,
|
||||
created_at=now,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
||||
def _get_provider(self, flaresolverr_url: str) -> FlareSolverrClearanceProvider:
|
||||
url = str(flaresolverr_url or "").strip().rstrip("/")
|
||||
with self._lock:
|
||||
provider = self._provider_cache.get(url)
|
||||
if provider is None:
|
||||
provider = self._clearance_provider_factory(url)
|
||||
self._provider_cache[url] = provider
|
||||
return provider
|
||||
|
||||
def _get_flight_lock(self, key: tuple[str, str]) -> threading.Lock:
|
||||
with self._lock:
|
||||
lock = self._flight_locks.get(key)
|
||||
if lock is None:
|
||||
lock = threading.Lock()
|
||||
self._flight_locks[key] = lock
|
||||
return lock
|
||||
|
||||
def _get_cached_bundle(self, key: tuple[str, str]) -> ClearanceBundle | None:
|
||||
with self._lock:
|
||||
return self._clearance_cache.get(key)
|
||||
|
||||
def _set_cached_bundle(self, key: tuple[str, str], bundle: ClearanceBundle) -> None:
|
||||
with self._lock:
|
||||
self._clearance_cache[key] = bundle
|
||||
|
||||
@staticmethod
|
||||
def _cache_key(proxy_url: str, target_host: str) -> tuple[str, str]:
|
||||
return (normalize_proxy_url(proxy_url), _normalize_host(target_host))
|
||||
|
||||
|
||||
def _clean(value: object) -> str:
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def _colon_proxy_to_url(url: str) -> str:
|
||||
parts = url.split(":", 3)
|
||||
if len(parts) == 4 and parts[1].isdigit():
|
||||
host, port, username, password = parts
|
||||
return f"http://{quote(username, safe='')}:{quote(password, safe='')}@{host}:{port}"
|
||||
if len(parts) == 2 and parts[1].isdigit():
|
||||
return f"http://{url}"
|
||||
return url
|
||||
|
||||
|
||||
def _normalize_host(host: str) -> str:
|
||||
return str(host or "").strip().strip(".").lower()
|
||||
|
||||
|
||||
def _host_from_url(url: str) -> str:
|
||||
candidate = str(url or "").strip()
|
||||
parsed = urlparse(candidate)
|
||||
if not parsed.hostname and candidate and "://" not in candidate:
|
||||
parsed = urlparse(f"https://{candidate}")
|
||||
return _normalize_host(parsed.hostname or "")
|
||||
|
||||
|
||||
def _status_codes_tuple(value: object) -> tuple[int, ...]:
|
||||
source = value if isinstance(value, list) else [403]
|
||||
codes: list[int] = []
|
||||
for item in source:
|
||||
if isinstance(item, bool):
|
||||
continue
|
||||
try:
|
||||
code = int(item)
|
||||
except (OverflowError, TypeError, ValueError):
|
||||
continue
|
||||
if 100 <= code <= 599 and code not in codes:
|
||||
codes.append(code)
|
||||
return tuple(codes or [403])
|
||||
|
||||
|
||||
def _coerce_timeout(value: object) -> float:
|
||||
try:
|
||||
timeout = float(value)
|
||||
except (OverflowError, TypeError, ValueError):
|
||||
timeout = 60.0
|
||||
return max(1.0, timeout)
|
||||
|
||||
|
||||
def _is_valid_proxy_url(url: str) -> bool:
|
||||
parsed = urlparse(normalize_proxy_url(url))
|
||||
return parsed.scheme in {"http", "https", "socks5", "socks5h"} and bool(parsed.netloc)
|
||||
|
||||
|
||||
def _domain_matches(host: str, domain: str) -> bool:
|
||||
normalized_host = _normalize_host(host)
|
||||
normalized_domain = _normalize_host(domain.lstrip("."))
|
||||
if not normalized_domain:
|
||||
return True
|
||||
return normalized_host == normalized_domain or normalized_host.endswith(f".{normalized_domain}")
|
||||
|
||||
|
||||
def _filter_flaresolverr_cookies(raw_cookies: object, target_host: str) -> dict[str, str]:
|
||||
if not isinstance(raw_cookies, list):
|
||||
return {}
|
||||
|
||||
filtered_cookies: dict[str, str] = {}
|
||||
for item in raw_cookies:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = str(item.get("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
value = str(item.get("value") or "")
|
||||
domain = str(item.get("domain") or "").strip()
|
||||
if not domain or _domain_matches(target_host, domain):
|
||||
filtered_cookies[name] = value
|
||||
return filtered_cookies
|
||||
|
||||
|
||||
def _parse_cookie_header(header: str) -> dict[str, str]:
|
||||
cookies: dict[str, str] = {}
|
||||
for part in str(header or "").split(";"):
|
||||
name, sep, value = part.strip().partition("=")
|
||||
if sep and name:
|
||||
cookies[name.strip()] = value.strip()
|
||||
return cookies
|
||||
|
||||
|
||||
def _cookies_to_header(cookies: Mapping[str, str]) -> str:
|
||||
return "; ".join(f"{name}={value}" for name, value in cookies.items() if name)
|
||||
|
||||
|
||||
def _merge_cookie_header(existing_header: str, cookies: Mapping[str, str]) -> str:
|
||||
existing = str(existing_header or "").strip()
|
||||
existing_names = set(_parse_cookie_header(existing).keys())
|
||||
additions = [f"{name}={value}" for name, value in cookies.items() if name and name not in existing_names]
|
||||
if existing and additions:
|
||||
return existing.rstrip("; ") + "; " + "; ".join(additions)
|
||||
if existing:
|
||||
return existing
|
||||
return "; ".join(additions)
|
||||
|
||||
|
||||
def _find_header_key(headers: Mapping[str, object], name: str) -> str | None:
|
||||
target = name.lower()
|
||||
for key in headers:
|
||||
if str(key).lower() == target:
|
||||
return str(key)
|
||||
return None
|
||||
|
||||
|
||||
def _redact_url_credentials(text: str) -> str:
|
||||
return re.sub(
|
||||
r"((?:https?|socks5h?|socks)://)([^\s/@:]+):([^\s/@]+)@",
|
||||
r"\1[REDACTED]@",
|
||||
str(text or ""),
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def test_proxy(url: str = "", *, timeout: float = 15.0) -> dict:
|
||||
candidate = normalize_proxy_url(_clean(url))
|
||||
proxy_source = "input"
|
||||
if not candidate:
|
||||
profile = proxy_settings.get_profile(upstream=True)
|
||||
candidate = profile.proxy_url
|
||||
proxy_source = profile.proxy_source
|
||||
result_base = {"proxy_source": proxy_source, "has_proxy": bool(candidate)}
|
||||
if not candidate:
|
||||
return {
|
||||
"ok": False,
|
||||
"status": 0,
|
||||
"latency_ms": 0,
|
||||
"error": "no active proxy configured",
|
||||
**result_base,
|
||||
}
|
||||
if not _is_valid_proxy_url(candidate):
|
||||
return {
|
||||
"ok": False,
|
||||
"status": 0,
|
||||
"latency_ms": 0,
|
||||
"error": "invalid proxy url",
|
||||
**result_base,
|
||||
}
|
||||
session = Session(impersonate="edge101", verify=True, proxy=candidate)
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
response = session.get(
|
||||
"https://chatgpt.com/api/auth/csrf",
|
||||
headers={"user-agent": "Mozilla/5.0 (chatgpt2api proxy test)"},
|
||||
timeout=timeout,
|
||||
)
|
||||
latency_ms = int((time.perf_counter() - started) * 1000)
|
||||
return {
|
||||
"ok": response.status_code < 500,
|
||||
"status": int(response.status_code),
|
||||
"latency_ms": latency_ms,
|
||||
"error": None if response.status_code < 500 else f"HTTP {response.status_code}",
|
||||
**result_base,
|
||||
}
|
||||
except Exception as exc:
|
||||
latency_ms = int((time.perf_counter() - started) * 1000)
|
||||
return {
|
||||
"ok": False,
|
||||
"status": 0,
|
||||
"latency_ms": latency_ms,
|
||||
"error": _redact_url_credentials(str(exc) or exc.__class__.__name__),
|
||||
**result_base,
|
||||
}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_clearance(target_url: str = "https://chatgpt.com") -> dict:
|
||||
target_url = str(target_url or "https://chatgpt.com").strip() or "https://chatgpt.com"
|
||||
started = time.perf_counter()
|
||||
status = proxy_settings.get_runtime_status()
|
||||
if not status.get("clearance_enabled"):
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "disabled",
|
||||
"latency_ms": 0,
|
||||
"has_cookies": False,
|
||||
"user_agent": "",
|
||||
"error": "clearance is disabled",
|
||||
"runtime": status,
|
||||
}
|
||||
try:
|
||||
bundle = proxy_settings.refresh_clearance(target_url=target_url, force=True, upstream=True)
|
||||
except Exception as exc:
|
||||
latency_ms = int((time.perf_counter() - started) * 1000)
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "error",
|
||||
"latency_ms": latency_ms,
|
||||
"has_cookies": False,
|
||||
"user_agent": "",
|
||||
"error": _redact_url_credentials(str(exc) or exc.__class__.__name__),
|
||||
"runtime": proxy_settings.get_runtime_status(),
|
||||
}
|
||||
|
||||
latency_ms = int((time.perf_counter() - started) * 1000)
|
||||
runtime = proxy_settings.get_runtime_status()
|
||||
if bundle is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"latency_ms": latency_ms,
|
||||
"has_cookies": False,
|
||||
"user_agent": "",
|
||||
"error": "clearance refresh returned no bundle",
|
||||
"runtime": runtime,
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"status": "ok",
|
||||
"latency_ms": latency_ms,
|
||||
"has_cookies": bool(bundle.cookies),
|
||||
"user_agent": bundle.user_agent or "",
|
||||
"error": None,
|
||||
"runtime": runtime,
|
||||
}
|
||||
|
||||
|
||||
proxy_settings = ProxySettingsStore()
|
||||
Reference in New Issue
Block a user