fix: use curl_cffi Chrome TLS impersonation for Claude provider

claude.ai has the same Cloudflare TLS fingerprinting protection as
chatgpt.com. Apply the same fix: curl_cffi impersonate=chrome120,
remove base class User-Agent to avoid JA3/UA mismatch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
JesseMarkowitz
2026-02-28 05:34:35 -05:00
parent 51c806c2c6
commit 23d7c17255
2 changed files with 21 additions and 13 deletions

View File

@@ -1,7 +1,7 @@
"""Debug script — tests Claude API connectivity.""" """Debug script — tests Claude API connectivity using curl_cffi Chrome impersonation."""
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import requests from curl_cffi import requests as curl_requests
load_dotenv() load_dotenv()
key = os.getenv("CLAUDE_SESSION_KEY") key = os.getenv("CLAUDE_SESSION_KEY")
@@ -9,16 +9,14 @@ if not key:
print("ERROR: CLAUDE_SESSION_KEY not found in .env") print("ERROR: CLAUDE_SESSION_KEY not found in .env")
raise SystemExit(1) raise SystemExit(1)
s = requests.Session() s = curl_requests.Session(impersonate="chrome120")
# Test 1: cookie as a jar entry (correct way)
s.cookies.set("sessionKey", key, domain="claude.ai", path="/") s.cookies.set("sessionKey", key, domain="claude.ai", path="/")
s.headers.update({ s.headers.update({
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Referer": "https://claude.ai/", "Referer": "https://claude.ai/",
"Accept": "application/json", "Accept": "application/json",
}) })
print("Calling /api/organizations (cookie jar) ...") print("Calling /api/organizations (with Chrome TLS impersonation) ...")
r = s.get("https://claude.ai/api/organizations", timeout=15) r = s.get("https://claude.ai/api/organizations", timeout=15)
print(f"Status: {r.status_code}") print(f"Status: {r.status_code}")
print(f"Response (first 300 chars): {r.text[:300]}") print(f"Response (first 400 chars): {r.text[:400]}")

View File

@@ -3,25 +3,35 @@
import logging import logging
import os import os
from curl_cffi import requests as curl_requests
from src.providers.base import BaseProvider, ProviderError from src.providers.base import BaseProvider, ProviderError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
BASE_URL = "https://claude.ai/api" BASE_URL = "https://claude.ai/api"
IMPERSONATE = "chrome120"
class ClaudeProvider(BaseProvider): class ClaudeProvider(BaseProvider):
"""Provider for Claude conversations via the internal web API. """Provider for Claude conversations via the internal web API.
Authentication: Cookie: sessionKey=<CLAUDE_SESSION_KEY> Uses curl_cffi to impersonate Chrome's TLS fingerprint, bypassing
Token: sessionKey cookie value from claude.ai. Cloudflare's bot detection (same issue as chatgpt.com).
Typical validity: ~30 days (opaque; expiry cannot be decoded client-side).
Authentication: sessionKey cookie (~30 day lifetime, opaque string).
Expiry cannot be decoded client-side — a 401 is the only signal.
""" """
provider_name = "claude" provider_name = "claude"
def __init__(self, session_key: str | None = None) -> None: def __init__(self, session_key: str | None = None) -> None:
super().__init__() cf_session = curl_requests.Session(impersonate=IMPERSONATE)
super().__init__(session=cf_session) # type: ignore[arg-type]
# Remove base class User-Agent so curl_cffi uses its Chrome-matched UA
self._session.headers.pop("User-Agent", None)
key = session_key or os.getenv("CLAUDE_SESSION_KEY", "").strip() key = session_key or os.getenv("CLAUDE_SESSION_KEY", "").strip()
if not key: if not key:
raise ProviderError( raise ProviderError(
@@ -32,7 +42,7 @@ class ClaudeProvider(BaseProvider):
"Run 'python -m src.main auth' to configure it." "Run 'python -m src.main auth' to configure it."
), ),
) )
# Set sessionKey in the cookie jar (not as a raw header string) # Set sessionKey in the cookie jar
self._session.cookies.set("sessionKey", key, domain="claude.ai", path="/") self._session.cookies.set("sessionKey", key, domain="claude.ai", path="/")
self._session.headers.update( self._session.headers.update(
{ {
@@ -41,7 +51,7 @@ class ClaudeProvider(BaseProvider):
} }
) )
self._org_id: str | None = None # cached per session self._org_id: str | None = None # cached per session
logger.debug("[claude] Session initialised (key: [REDACTED])") logger.debug("[claude] Session initialised with Chrome TLS impersonation (key: [REDACTED])")
def _handle_401(self) -> None: def _handle_401(self) -> None:
msg = ( msg = (