From b774270704f9d37f411fcc076ae4a16795783cf4 Mon Sep 17 00:00:00 2001 From: zqq61 <1852150449@qq.com> Date: Mon, 16 Mar 2026 00:55:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A8http.client=E6=9B=BF=E4=BB=A3ht?= =?UTF-8?q?tpx=E8=AE=A4=E8=AF=81=EF=BC=8C=E7=BB=95=E8=BF=87CF=E6=8C=87?= =?UTF-8?q?=E7=BA=B9=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 认证请求改用stdlib http.client,避免httpx TLS指纹被Cloudflare拦截 - login_as为空时不传x-login-as header - 服务器直连api.airwallex.com已验证201成功 --- backend/app/proxy_client.py | 59 +++++++++++++---------- backend/app/services/airwallex_service.py | 2 +- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/backend/app/proxy_client.py b/backend/app/proxy_client.py index bf44882..1f2514d 100644 --- a/backend/app/proxy_client.py +++ b/backend/app/proxy_client.py @@ -1,8 +1,13 @@ -"""Airwallex client with proxy support (HTTP and SOCKS5).""" +"""Airwallex client with proxy support (HTTP and SOCKS5). +Uses http.client for authentication to avoid Cloudflare TLS fingerprint blocking. +""" +import http.client +import json import logging +import ssl import httpx from datetime import datetime, timezone, timedelta -from typing import Optional +from urllib.parse import urlparse from airwallex.client import AirwallexClient logger = logging.getLogger(__name__) @@ -10,14 +15,13 @@ logger = logging.getLogger(__name__) class ProxiedAirwallexClient(AirwallexClient): """AirwallexClient that routes requests through an HTTP or SOCKS5 proxy. - Also supports x-login-as header for connected accounts. + Uses stdlib http.client for auth to bypass Cloudflare fingerprinting. """ def __init__(self, proxy_url: str | None = None, login_as: str | None = None, **kwargs): self._proxy_url = proxy_url self._login_as = login_as super().__init__(**kwargs) - # Replace the default httpx client with a proxied one if proxy_url: self._client.close() self._client = httpx.Client( @@ -31,48 +35,53 @@ class ProxiedAirwallexClient(AirwallexClient): logger.info("Airwallex client initialized without proxy") def authenticate(self) -> None: - """Override authenticate to use proxy and x-login-as for auth requests.""" + """Authenticate using stdlib http.client to avoid CF blocking.""" if self._token and self._token_expiry and datetime.now(timezone.utc) < self._token_expiry: return - logger.info("Authenticating with Airwallex API at %s (proxy: %s, login_as: %s)", - self.auth_url, bool(self._proxy_url), self._login_as or "none") + parsed = urlparse(self.auth_url) + host = parsed.hostname + path = parsed.path + port = parsed.port or (443 if parsed.scheme == "https" else 80) - auth_kwargs: dict = {"timeout": self.request_timeout} - if self._proxy_url: - auth_kwargs["proxy"] = self._proxy_url + logger.info("Authenticating at %s (via http.client, login_as: %s)", self.auth_url, self._login_as or "none") - auth_headers = { + headers = { "Content-Type": "application/json", "x-client-id": self.client_id, "x-api-key": self.api_key, } if self._login_as: - auth_headers["x-login-as"] = self._login_as + headers["x-login-as"] = self._login_as - auth_client = httpx.Client(**auth_kwargs) try: - response = auth_client.post( - self.auth_url, - headers=auth_headers, - content="{}", - ) + if parsed.scheme == "https": + ctx = ssl.create_default_context() + conn = http.client.HTTPSConnection(host, port, timeout=self.request_timeout, context=ctx) + else: + conn = http.client.HTTPConnection(host, port, timeout=self.request_timeout) - if response.status_code != 200: - body = response.text[:300] - logger.error("Auth failed: HTTP %d, body: %s", response.status_code, body) - response.raise_for_status() + conn.request("POST", path, json.dumps({}).encode("utf-8"), headers) + resp = conn.getresponse() + body = resp.read().decode("utf-8") - data = response.json() + if resp.status not in (200, 201): + logger.error("Auth failed: HTTP %d, body: %s", resp.status, body[:300]) + raise Exception(f"Airwallex 认证失败 (HTTP {resp.status}): {body[:200]}") + + data = json.loads(body) self._token = data.get("token") self._token_expiry = datetime.now(timezone.utc) + timedelta(minutes=28) logger.info("Authentication successful, token expires in 28 minutes") finally: - auth_client.close() + try: + conn.close() + except Exception: + pass @property def headers(self): - """Add x-login-as to all API request headers.""" + """Add x-login-as to all API request headers if configured.""" h = super().headers if self._login_as: h["x-login-as"] = self._login_as diff --git a/backend/app/services/airwallex_service.py b/backend/app/services/airwallex_service.py index 5f2c965..4c47fcc 100644 --- a/backend/app/services/airwallex_service.py +++ b/backend/app/services/airwallex_service.py @@ -44,7 +44,7 @@ def get_client(db: Session) -> ProxiedAirwallexClient: client_id = _get_setting(db, "airwallex_client_id") api_key = _get_setting(db, "airwallex_api_key") base_url = _get_setting(db, "airwallex_base_url", "https://api-demo.airwallex.com/") - login_as = _get_setting(db, "airwallex_login_as") + login_as = _get_setting(db, "airwallex_login_as").strip() or None proxy_url = _build_proxy_url(db) if not client_id or not api_key: