fix: 用http.client替代httpx认证,绕过CF指纹拦截

- 认证请求改用stdlib http.client,避免httpx TLS指纹被Cloudflare拦截
- login_as为空时不传x-login-as header
- 服务器直连api.airwallex.com已验证201成功
This commit is contained in:
zqq61
2026-03-16 00:55:34 +08:00
parent 81d9c3a7e1
commit b774270704
2 changed files with 35 additions and 26 deletions

View File

@@ -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

View File

@@ -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: