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:
@@ -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]}")
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
Reference in New Issue
Block a user