""" Base REST client helpers for direct exchange connections. Notes: - Keep this minimal and dependency-light (requests only). - All secrets must be excluded from logs. """ from __future__ import annotations import json import logging import os import time from dataclasses import dataclass from typing import Any, Dict, Optional, Tuple, Union import requests logger = logging.getLogger(__name__) # Cached SSL verify setting for all live-trading REST calls (requests + SOCKS proxy). _requests_verify_value: Optional[Union[bool, str]] = None _ssl_verify_disabled_logged = False # OS CA bundles (Docker / slim images: install ``ca-certificates``; corporate roots often added here too). _SYSTEM_CA_BUNDLE_CANDIDATES: Tuple[str, ...] = ( "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu "/etc/ssl/cert.pem", # Alpine, some slim images "/etc/pki/tls/certs/ca-bundle.crt", # RHEL/Fedora ) def _get_requests_verify() -> Union[bool, str]: """ Resolve ``verify`` for ``requests`` when calling exchanges through proxies (e.g. PROXY_URL=socks5h://...). - LIVE_TRADING_SSL_VERIFY=0|false|no|off: disable verification (insecure; mitm risk). - LIVE_TRADING_CA_BUNDLE / REQUESTS_CA_BUNDLE / SSL_CERT_FILE / CURL_CA_BUNDLE: path to a PEM CA bundle (needed for corporate TLS inspection or custom roots). - Else a non-empty OS CA file if present (helps Gate/HTX/hbdm etc. in minimal images). - Otherwise certifi's bundle when available. """ global _requests_verify_value, _ssl_verify_disabled_logged if _requests_verify_value is not None: return _requests_verify_value flag = (os.environ.get("LIVE_TRADING_SSL_VERIFY") or "").strip().lower() if flag in ("0", "false", "no", "off"): if not _ssl_verify_disabled_logged: logger.warning( "LIVE_TRADING_SSL_VERIFY is disabled: HTTPS certificate verification is OFF for live trading " "requests (MITM risk). Fix CA trust or set LIVE_TRADING_CA_BUNDLE instead for production." ) _ssl_verify_disabled_logged = True _requests_verify_value = False return _requests_verify_value for key in ("LIVE_TRADING_CA_BUNDLE", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE", "CURL_CA_BUNDLE"): path = (os.environ.get(key) or "").strip() if path and os.path.isfile(path): _requests_verify_value = path return _requests_verify_value for path in _SYSTEM_CA_BUNDLE_CANDIDATES: try: if path and os.path.isfile(path) and os.path.getsize(path) >= 256: _requests_verify_value = path return _requests_verify_value except OSError: continue try: import certifi _requests_verify_value = certifi.where() except ImportError: _requests_verify_value = True return _requests_verify_value @dataclass class LiveOrderResult: exchange_id: str exchange_order_id: str filled: float avg_price: float raw: Dict[str, Any] class LiveTradingError(Exception): pass class BaseRestClient: def __init__(self, base_url: str, timeout_sec: float = 15.0): self.base_url = (base_url or "").rstrip("/") self.timeout_sec = float(timeout_sec) def _url(self, path: str) -> str: p = str(path or "") if not p.startswith("/"): p = "/" + p return f"{self.base_url}{p}" def _request( self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, data: Optional[Any] = None, ) -> Tuple[int, Dict[str, Any], str]: url = self._url(path) try: resp = requests.request( method=str(method or "GET").upper(), url=url, params=params or None, json=json_body if json_body is not None else None, data=data, headers=headers or None, timeout=self.timeout_sec, verify=_get_requests_verify(), ) except requests.exceptions.SSLError as e: logger.warning( "Exchange HTTPS TLS verify failed (%s). Same setting applies to all REST exchanges (Gate, HTX/hbdm, etc.). " "Behind PROXY_URL/SOCKS or TLS inspection: set LIVE_TRADING_CA_BUNDLE to a PEM bundle (or REQUESTS_CA_BUNDLE), " "ensure ca-certificates in the image, or dev-only LIVE_TRADING_SSL_VERIFY=false. %s", url, e, ) raise text = resp.text or "" parsed: Dict[str, Any] = {} try: parsed = resp.json() if text else {} except Exception: parsed = {"raw_text": text[:2000]} return int(resp.status_code), parsed, text @staticmethod def _now_ms() -> int: return int(time.time() * 1000) @staticmethod def _json_dumps(obj: Any) -> str: return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))