143 lines
5.2 KiB
Python
143 lines
5.2 KiB
Python
import threading
|
|
import time
|
|
from urllib.parse import quote
|
|
from urllib.request import getproxies
|
|
|
|
import httpx
|
|
|
|
from server.services.device import DeviceProfile
|
|
|
|
|
|
class HttpClient:
|
|
def __init__(self) -> None:
|
|
self._proxy = {
|
|
"mode": "off",
|
|
"scheme": "http",
|
|
"host": "",
|
|
"port": "",
|
|
"username": "",
|
|
"password": "",
|
|
}
|
|
self._client: httpx.Client | None = None
|
|
self._lock = threading.Lock()
|
|
|
|
def configure_proxy(self, proxy: dict) -> None:
|
|
data = proxy or {}
|
|
mode = data.get("mode") or "off"
|
|
if mode not in ("off", "system", "custom"):
|
|
raise RuntimeError("代理方式无效")
|
|
scheme = data.get("scheme") or "http"
|
|
if scheme not in ("http", "socks5"):
|
|
raise RuntimeError("代理协议无效")
|
|
host = (data.get("host") or "").strip()
|
|
port = str(data.get("port") or "").strip()
|
|
if mode == "custom":
|
|
if not host:
|
|
raise RuntimeError("请填写代理地址")
|
|
if not port.isdigit() or not 1 <= int(port) <= 65535:
|
|
raise RuntimeError("请填写 1 到 65535 的端口")
|
|
with self._lock:
|
|
self._proxy = {
|
|
"mode": mode,
|
|
"scheme": scheme,
|
|
"host": host,
|
|
"port": port,
|
|
"username": (data.get("username") or "").strip(),
|
|
"password": data.get("password") or "",
|
|
}
|
|
self._close_locked()
|
|
|
|
def request_json(self, method: str, url: str, token: str, device: DeviceProfile, body: dict | None) -> dict:
|
|
data = self.request_data(method, url, token, device, body)
|
|
if not isinstance(data, dict):
|
|
raise RuntimeError("接口没有返回数据")
|
|
return data
|
|
|
|
def request_data(self, method: str, url: str, token: str, device: DeviceProfile, body: dict | None):
|
|
headers = device.headers()
|
|
if token:
|
|
headers["apasstk"] = token
|
|
response = self._request(method, url, headers=headers, json=body)
|
|
payload = response.json()
|
|
if payload.get("code") != 200:
|
|
raise RuntimeError(f"{method} {url} -> {payload.get('code')} {payload.get('msg')}")
|
|
if "data" not in payload:
|
|
raise RuntimeError("接口没有返回数据")
|
|
return payload.get("data")
|
|
|
|
def _request(self, method: str, url: str, headers: dict, json: dict | None) -> httpx.Response:
|
|
last_error = None
|
|
for attempt in range(6):
|
|
try:
|
|
return self._client_locked().request(method, url, headers=headers, json=json)
|
|
except (httpx.HTTPError, TimeoutError, OSError) as exc:
|
|
last_error = exc
|
|
time.sleep(min(10, 1.5 * (attempt + 1)))
|
|
raise RuntimeError(f"请求失败: {last_error}")
|
|
|
|
def _client_locked(self) -> httpx.Client:
|
|
with self._lock:
|
|
if self._client is None or self._client.is_closed:
|
|
self._client = httpx.Client(**self._client_kwargs())
|
|
return self._client
|
|
|
|
def get_bytes(self, url: str) -> tuple[bytes, str]:
|
|
response = self._client_locked().get(url, headers={"User-Agent": "RaptDrama"})
|
|
response.raise_for_status()
|
|
media = response.headers.get("content-type", "image/jpeg").split(";", 1)[0].strip() or "image/jpeg"
|
|
if not media.startswith("image/"):
|
|
raise RuntimeError("不是图片")
|
|
return response.content, media
|
|
|
|
def _close_locked(self) -> None:
|
|
if self._client is not None:
|
|
self._client.close()
|
|
self._client = None
|
|
|
|
def _client_kwargs(self) -> dict:
|
|
kwargs = {
|
|
"trust_env": False,
|
|
"follow_redirects": True,
|
|
"timeout": httpx.Timeout(25.0),
|
|
}
|
|
mode = self._proxy["mode"]
|
|
if mode == "off":
|
|
kwargs["proxy"] = None
|
|
return kwargs
|
|
if mode == "custom":
|
|
kwargs["proxy"] = self._proxy_url()
|
|
return kwargs
|
|
http_proxy, https_proxy = self._system_proxies()
|
|
if http_proxy and https_proxy and http_proxy != https_proxy:
|
|
kwargs["mounts"] = {
|
|
"http://": httpx.HTTPTransport(proxy=http_proxy),
|
|
"https://": httpx.HTTPTransport(proxy=https_proxy),
|
|
}
|
|
return kwargs
|
|
kwargs["proxy"] = https_proxy or http_proxy or None
|
|
return kwargs
|
|
|
|
def _proxy_url(self) -> str:
|
|
scheme = "socks5h" if self._proxy["scheme"] == "socks5" else "http"
|
|
auth = ""
|
|
username = self._proxy["username"]
|
|
if username:
|
|
password = quote(self._proxy["password"] or "", safe="")
|
|
auth = f"{quote(username, safe='')}:{password}@"
|
|
return f"{scheme}://{auth}{self._proxy['host']}:{self._proxy['port']}"
|
|
|
|
@staticmethod
|
|
def _system_proxies() -> tuple[str, str]:
|
|
found = getproxies()
|
|
return _with_scheme(found.get("http")), _with_scheme(found.get("https"))
|
|
|
|
|
|
def _with_scheme(value: str | None) -> str:
|
|
text = (value or "").strip()
|
|
if text and "://" not in text:
|
|
return "http://" + text
|
|
return text
|
|
|
|
|
|
http = HttpClient()
|