f
This commit is contained in:
135
API/HTTP.py
135
API/HTTP.py
@@ -32,122 +32,8 @@ except Exception: # pragma: no cover - optional dependency
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_verify_value(verify_ssl: bool) -> Union[bool, str]:
|
||||
"""Return the httpx verify argument, preferring system-aware bundles.
|
||||
|
||||
Order of precedence:
|
||||
1. If verify_ssl is not True (False or path), return it.
|
||||
2. Respect existing SSL_CERT_FILE env var if present.
|
||||
3. Prefer `pip_system_certs` if present and it exposes a bundle path.
|
||||
4. Prefer `certifi_win32`/similar helpers by invoking them and reading certifi.where().
|
||||
5. Fall back to `certifi.where()` if available.
|
||||
6. Otherwise, return True to let httpx use system defaults.
|
||||
"""
|
||||
if verify_ssl is not True:
|
||||
return verify_ssl
|
||||
|
||||
env_cert = os.environ.get("SSL_CERT_FILE")
|
||||
if env_cert:
|
||||
return env_cert
|
||||
|
||||
def _try_module_bundle(mod_name: str) -> Optional[str]:
|
||||
# Prefer checking sys.modules first (helps test injection / monkeypatching)
|
||||
mod = sys.modules.get(mod_name)
|
||||
if mod is None:
|
||||
# Avoid raising ModuleNotFoundError so debuggers and callers aren't interrupted.
|
||||
# Check for module availability before attempting to import it.
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec(mod_name)
|
||||
if spec is None:
|
||||
return None
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module(mod_name)
|
||||
except Exception:
|
||||
# Treat any import/initialization failure as module not available.
|
||||
return None
|
||||
|
||||
# Common APIs that return a bundle path
|
||||
for attr in ("where", "get_ca_bundle", "bundle_path", "get_bundle_path", "get_bundle"):
|
||||
fn = getattr(mod, attr, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
res = fn()
|
||||
if res:
|
||||
return res
|
||||
except Exception:
|
||||
continue
|
||||
elif isinstance(fn, str) and fn:
|
||||
return fn
|
||||
|
||||
# Some helpers (e.g., certifi_win32) expose an action to merge system certs
|
||||
for call_attr in ("add_windows_store_certs", "add_system_certs", "merge_system_certs"):
|
||||
fn = getattr(mod, call_attr, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
fn()
|
||||
try:
|
||||
import certifi as _certifi
|
||||
|
||||
res = _certifi.where()
|
||||
if res:
|
||||
return res
|
||||
except Exception:
|
||||
logger.exception("Failed while probing certifi helper inner block")
|
||||
except Exception:
|
||||
logger.exception("Failed while invoking cert helper function")
|
||||
return None
|
||||
|
||||
# Prefer helpful modules if available (use safe checks to avoid first-chance import errors)
|
||||
for mod_name in ("pip_system_certs", "certifi_win32"):
|
||||
path = _try_module_bundle(mod_name)
|
||||
if path:
|
||||
try:
|
||||
os.environ["SSL_CERT_FILE"] = path
|
||||
except Exception:
|
||||
logger.exception("Failed to set SSL_CERT_FILE environment variable")
|
||||
logger.info(f"SSL_CERT_FILE not set; using bundle from {mod_name}: {path}")
|
||||
return path
|
||||
|
||||
# Fallback to certifi
|
||||
try:
|
||||
import certifi # type: ignore
|
||||
|
||||
path = certifi.where()
|
||||
if path:
|
||||
try:
|
||||
os.environ["SSL_CERT_FILE"] = path
|
||||
except Exception:
|
||||
logger.exception("Failed to set SSL_CERT_FILE environment variable during certifi fallback")
|
||||
logger.info(f"SSL_CERT_FILE not set; using certifi bundle: {path}")
|
||||
return path
|
||||
except Exception:
|
||||
logger.exception("Failed to probe certifi for trust bundle")
|
||||
|
||||
# Fallback to certifi
|
||||
try:
|
||||
import certifi # type: ignore
|
||||
|
||||
path = certifi.where()
|
||||
if path:
|
||||
try:
|
||||
os.environ["SSL_CERT_FILE"] = path
|
||||
except Exception:
|
||||
logger.exception("Failed to set SSL_CERT_FILE environment variable during certifi fallback")
|
||||
logger.info(f"SSL_CERT_FILE not set; using certifi bundle: {path}")
|
||||
return path
|
||||
except Exception:
|
||||
logger.exception("Failed to probe certifi for trust bundle")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_requests_verify_value(verify_ssl: bool = True) -> Union[bool, str]:
|
||||
"""Expose the verified value for reuse outside of HTTPClient (requests sessions)."""
|
||||
return _resolve_verify_value(verify_ssl)
|
||||
from API.ssl_certs import resolve_verify_value as _resolve_verify_value
|
||||
from API.ssl_certs import get_requests_verify_value
|
||||
|
||||
# Default configuration
|
||||
DEFAULT_TIMEOUT = 30.0
|
||||
@@ -444,13 +330,16 @@ class HTTPClient:
|
||||
"HTTPClient must be used with context manager (with statement)"
|
||||
)
|
||||
|
||||
# Merge headers
|
||||
if "headers" in kwargs and kwargs["headers"]:
|
||||
headers = self._get_headers()
|
||||
headers.update(kwargs["headers"])
|
||||
kwargs["headers"] = headers
|
||||
else:
|
||||
kwargs["headers"] = self._get_headers()
|
||||
# Merge headers once per call (do not rebuild for every retry attempt).
|
||||
merged_headers = self._get_headers()
|
||||
extra_headers = kwargs.get("headers")
|
||||
if extra_headers:
|
||||
try:
|
||||
merged_headers.update(extra_headers)
|
||||
except Exception:
|
||||
# If headers is not a mapping, keep it as-is and let httpx raise.
|
||||
merged_headers = extra_headers
|
||||
kwargs["headers"] = merged_headers
|
||||
|
||||
last_exception: Exception | None = None
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"(rapidgator\\.net/file/[0-9]{7,8})"
|
||||
],
|
||||
"regexp": "((rapidgator\\.net|rg\\.to|rapidgator\\.asia)/file/([0-9a-zA-Z]{32}))|((rapidgator\\.net/file/[0-9]{7,8}))",
|
||||
"status": false
|
||||
"status": true
|
||||
},
|
||||
"turbobit": {
|
||||
"name": "turbobit",
|
||||
@@ -222,20 +222,6 @@
|
||||
],
|
||||
"regexp": "(dailyuploads\\.net/[0-9a-zA-Z]{12})"
|
||||
},
|
||||
"ddl": {
|
||||
"name": "ddl",
|
||||
"type": "premium",
|
||||
"domains": [
|
||||
"ddl.to",
|
||||
"ddownload.com"
|
||||
],
|
||||
"regexps": [
|
||||
"(ddownload\\.com/[0-9a-zA-Z]{12})",
|
||||
"ddl\\.to/([0-9a-zA-Z]{12})"
|
||||
],
|
||||
"regexp": "((ddownload\\.com/[0-9a-zA-Z]{12}))|(ddl\\.to/([0-9a-zA-Z]{12}))",
|
||||
"status": false
|
||||
},
|
||||
"dropapk": {
|
||||
"name": "dropapk",
|
||||
"type": "premium",
|
||||
|
||||
58
API/httpx_shared.py
Normal file
58
API/httpx_shared.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Shared `httpx.Client` helper.
|
||||
|
||||
Creating short-lived httpx clients disables connection pooling and costs extra CPU.
|
||||
This module provides a small singleton client for callers that just need basic
|
||||
GETs without the full HTTPClient wrapper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from API.ssl_certs import resolve_verify_value
|
||||
|
||||
_DEFAULT_USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36"
|
||||
)
|
||||
|
||||
_lock = threading.Lock()
|
||||
_shared_client: Optional[httpx.Client] = None
|
||||
|
||||
|
||||
def get_shared_httpx_client(
|
||||
*,
|
||||
timeout: float = 30.0,
|
||||
verify_ssl: bool = True,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> httpx.Client:
|
||||
"""Return a process-wide shared synchronous httpx.Client."""
|
||||
|
||||
global _shared_client
|
||||
if _shared_client is None:
|
||||
with _lock:
|
||||
if _shared_client is None:
|
||||
base_headers = {"User-Agent": _DEFAULT_USER_AGENT}
|
||||
if headers:
|
||||
base_headers.update({str(k): str(v) for k, v in headers.items()})
|
||||
_shared_client = httpx.Client(
|
||||
timeout=timeout,
|
||||
verify=resolve_verify_value(verify_ssl),
|
||||
headers=base_headers,
|
||||
)
|
||||
|
||||
return _shared_client
|
||||
|
||||
|
||||
def close_shared_httpx_client() -> None:
|
||||
global _shared_client
|
||||
client = _shared_client
|
||||
_shared_client = None
|
||||
if client is not None:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
68
API/requests_client.py
Normal file
68
API/requests_client.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Shared `requests` session helper.
|
||||
|
||||
Many providers still use `requests` directly. Reusing a Session provides:
|
||||
- Connection pooling (fewer TCP/TLS handshakes)
|
||||
- Lower CPU overhead per request
|
||||
|
||||
This module intentionally avoids importing the heavy httpx-based stack.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from API.ssl_certs import resolve_verify_value
|
||||
|
||||
_DEFAULT_USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36"
|
||||
)
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
def get_requests_session(
|
||||
*,
|
||||
user_agent: str = _DEFAULT_USER_AGENT,
|
||||
verify_ssl: bool = True,
|
||||
pool_connections: int = 16,
|
||||
pool_maxsize: int = 16,
|
||||
) -> requests.Session:
|
||||
"""Return a thread-local shared Session configured for pooling."""
|
||||
|
||||
session: Optional[requests.Session] = getattr(_local, "session", None)
|
||||
if session is not None:
|
||||
return session
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({"User-Agent": str(user_agent or _DEFAULT_USER_AGENT)})
|
||||
|
||||
# Expand connection pool; keep max_retries=0 to avoid semantic changes.
|
||||
adapter = HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize, max_retries=0)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
# Configure verify once.
|
||||
session.verify = resolve_verify_value(verify_ssl)
|
||||
|
||||
_local.session = session
|
||||
return session
|
||||
|
||||
|
||||
def request(
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[float] = None,
|
||||
**kwargs: Any,
|
||||
) -> requests.Response:
|
||||
"""Convenience wrapper around the shared Session."""
|
||||
|
||||
sess = get_requests_session()
|
||||
return sess.request(method, url, params=params, headers=headers, timeout=timeout, **kwargs)
|
||||
109
API/ssl_certs.py
Normal file
109
API/ssl_certs.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""SSL certificate bundle resolution helpers.
|
||||
|
||||
This module is intentionally lightweight (no httpx import) so it can be used by
|
||||
providers that still rely on `requests` without paying the import cost of the
|
||||
full HTTP client stack.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resolve_verify_value(verify_ssl: bool) -> Union[bool, str]:
|
||||
"""Return the value suitable for `requests`/`httpx` verify parameters.
|
||||
|
||||
- If verify_ssl is not True (False or a path-like string), it is returned.
|
||||
- Respects an existing SSL_CERT_FILE env var.
|
||||
- Tries optional helpers (`pip_system_certs`, `certifi_win32`).
|
||||
- Falls back to `certifi.where()`.
|
||||
- Otherwise returns True.
|
||||
"""
|
||||
|
||||
if verify_ssl is not True:
|
||||
return verify_ssl
|
||||
|
||||
env_cert = os.environ.get("SSL_CERT_FILE")
|
||||
if env_cert:
|
||||
return env_cert
|
||||
|
||||
def _try_module_bundle(mod_name: str) -> Optional[str]:
|
||||
mod = sys.modules.get(mod_name)
|
||||
if mod is None:
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.find_spec(mod_name)
|
||||
if spec is None:
|
||||
return None
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module(mod_name)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for attr in ("where", "get_ca_bundle", "bundle_path", "get_bundle_path", "get_bundle"):
|
||||
fn = getattr(mod, attr, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
res = fn()
|
||||
if res:
|
||||
return str(res)
|
||||
except Exception:
|
||||
continue
|
||||
elif isinstance(fn, str) and fn:
|
||||
return fn
|
||||
|
||||
for call_attr in ("add_windows_store_certs", "add_system_certs", "merge_system_certs"):
|
||||
fn = getattr(mod, call_attr, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
fn()
|
||||
try:
|
||||
import certifi as _certifi # type: ignore
|
||||
|
||||
res = _certifi.where()
|
||||
if res:
|
||||
return str(res)
|
||||
except Exception:
|
||||
logger.exception("Failed while probing certifi helper inner block")
|
||||
except Exception:
|
||||
logger.exception("Failed while invoking cert helper function")
|
||||
return None
|
||||
|
||||
for mod_name in ("pip_system_certs", "certifi_win32"):
|
||||
path = _try_module_bundle(mod_name)
|
||||
if path:
|
||||
try:
|
||||
os.environ["SSL_CERT_FILE"] = path
|
||||
except Exception:
|
||||
logger.exception("Failed to set SSL_CERT_FILE environment variable")
|
||||
logger.info(f"SSL_CERT_FILE not set; using bundle from {mod_name}: {path}")
|
||||
return path
|
||||
|
||||
try:
|
||||
import certifi # type: ignore
|
||||
|
||||
path = certifi.where()
|
||||
if path:
|
||||
try:
|
||||
os.environ["SSL_CERT_FILE"] = path
|
||||
except Exception:
|
||||
logger.exception("Failed to set SSL_CERT_FILE environment variable during certifi fallback")
|
||||
logger.info(f"SSL_CERT_FILE not set; using certifi bundle: {path}")
|
||||
return str(path)
|
||||
except Exception:
|
||||
logger.exception("Failed to probe certifi for trust bundle")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_requests_verify_value(verify_ssl: bool = True) -> Union[bool, str]:
|
||||
"""Backwards-friendly alias for call sites that only care about requests."""
|
||||
|
||||
return resolve_verify_value(verify_ssl)
|
||||
Reference in New Issue
Block a user