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:
@@ -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 logging
|
||||||
|
import ssl
|
||||||
import httpx
|
import httpx
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional
|
from urllib.parse import urlparse
|
||||||
from airwallex.client import AirwallexClient
|
from airwallex.client import AirwallexClient
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -10,14 +15,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ProxiedAirwallexClient(AirwallexClient):
|
class ProxiedAirwallexClient(AirwallexClient):
|
||||||
"""AirwallexClient that routes requests through an HTTP or SOCKS5 proxy.
|
"""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):
|
def __init__(self, proxy_url: str | None = None, login_as: str | None = None, **kwargs):
|
||||||
self._proxy_url = proxy_url
|
self._proxy_url = proxy_url
|
||||||
self._login_as = login_as
|
self._login_as = login_as
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
# Replace the default httpx client with a proxied one
|
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
self._client.close()
|
self._client.close()
|
||||||
self._client = httpx.Client(
|
self._client = httpx.Client(
|
||||||
@@ -31,48 +35,53 @@ class ProxiedAirwallexClient(AirwallexClient):
|
|||||||
logger.info("Airwallex client initialized without proxy")
|
logger.info("Airwallex client initialized without proxy")
|
||||||
|
|
||||||
def authenticate(self) -> None:
|
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:
|
if self._token and self._token_expiry and datetime.now(timezone.utc) < self._token_expiry:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Authenticating with Airwallex API at %s (proxy: %s, login_as: %s)",
|
parsed = urlparse(self.auth_url)
|
||||||
self.auth_url, bool(self._proxy_url), self._login_as or "none")
|
host = parsed.hostname
|
||||||
|
path = parsed.path
|
||||||
|
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||||
|
|
||||||
auth_kwargs: dict = {"timeout": self.request_timeout}
|
logger.info("Authenticating at %s (via http.client, login_as: %s)", self.auth_url, self._login_as or "none")
|
||||||
if self._proxy_url:
|
|
||||||
auth_kwargs["proxy"] = self._proxy_url
|
|
||||||
|
|
||||||
auth_headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-client-id": self.client_id,
|
"x-client-id": self.client_id,
|
||||||
"x-api-key": self.api_key,
|
"x-api-key": self.api_key,
|
||||||
}
|
}
|
||||||
if self._login_as:
|
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:
|
try:
|
||||||
response = auth_client.post(
|
if parsed.scheme == "https":
|
||||||
self.auth_url,
|
ctx = ssl.create_default_context()
|
||||||
headers=auth_headers,
|
conn = http.client.HTTPSConnection(host, port, timeout=self.request_timeout, context=ctx)
|
||||||
content="{}",
|
else:
|
||||||
)
|
conn = http.client.HTTPConnection(host, port, timeout=self.request_timeout)
|
||||||
|
|
||||||
if response.status_code != 200:
|
conn.request("POST", path, json.dumps({}).encode("utf-8"), headers)
|
||||||
body = response.text[:300]
|
resp = conn.getresponse()
|
||||||
logger.error("Auth failed: HTTP %d, body: %s", response.status_code, body)
|
body = resp.read().decode("utf-8")
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
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 = data.get("token")
|
||||||
self._token_expiry = datetime.now(timezone.utc) + timedelta(minutes=28)
|
self._token_expiry = datetime.now(timezone.utc) + timedelta(minutes=28)
|
||||||
logger.info("Authentication successful, token expires in 28 minutes")
|
logger.info("Authentication successful, token expires in 28 minutes")
|
||||||
finally:
|
finally:
|
||||||
auth_client.close()
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self):
|
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
|
h = super().headers
|
||||||
if self._login_as:
|
if self._login_as:
|
||||||
h["x-login-as"] = self._login_as
|
h["x-login-as"] = self._login_as
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def get_client(db: Session) -> ProxiedAirwallexClient:
|
|||||||
client_id = _get_setting(db, "airwallex_client_id")
|
client_id = _get_setting(db, "airwallex_client_id")
|
||||||
api_key = _get_setting(db, "airwallex_api_key")
|
api_key = _get_setting(db, "airwallex_api_key")
|
||||||
base_url = _get_setting(db, "airwallex_base_url", "https://api-demo.airwallex.com/")
|
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)
|
proxy_url = _build_proxy_url(db)
|
||||||
|
|
||||||
if not client_id or not api_key:
|
if not client_id or not api_key:
|
||||||
|
|||||||
Reference in New Issue
Block a user