Merge branch 'dev'

This commit is contained in:
Sumit Gautam
2026-06-27 21:45:24 +05:30
3 changed files with 70 additions and 16 deletions
+14 -15
View File
@@ -38,6 +38,7 @@ from urllib.parse import parse_qs, urlparse
from playwright.sync_api import sync_playwright, Error as PlaywrightError
from .auth import DEFAULT_AUTH_FILE, DEFAULT_PROFILE_DIR
from .useragent import CHROME_UA
COPILOT_URL = "https://copilot.microsoft.com/"
@@ -46,17 +47,14 @@ COPILOT_URL = "https://copilot.microsoft.com/"
# We reach into that frame to click its checkbox — see _click_turnstile.
_TURNSTILE_IFRAME = "iframe[src*='challenges.cloudflare.com'], iframe[src*='turnstile']"
# A stock desktop Chrome UA used in headless mode. Headless Chromium otherwise
# advertises "HeadlessChrome/..." in both the request UA header and
# navigator.userAgent — a blatant bot signal to Cloudflare Turnstile, and a UA
# that can mismatch the cf_clearance UA-binding the curl_cffi driver relies on
# (clearance is bound to the UA that earned it). Pinning a normal Chrome UA makes
# the session look ordinary and keeps the earned clearance reusable. Bump the
# version occasionally to stay current.
_STEALTH_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
)
# The one UA every browser context advertises — the same string the curl_cffi
# driver presents (see copilot/useragent.py). Applied to *both* headless and
# visible launches so clearance earned by either is reusable by the driver:
# cf_clearance is bound to the earning UA, so they must all match. It also hides
# the "HeadlessChrome/..." token headless Chromium otherwise leaks (a blatant bot
# tell). Because CHROME_UA tracks Playwright's bundled Chromium version, the
# override doesn't contradict the browser's native Sec-CH-UA client hint.
_STEALTH_UA = CHROME_UA
# Injected into every frame to hide the residual automation tell that survives
# --disable-blink-features=AutomationControlled in some Chromium builds.
@@ -189,10 +187,11 @@ class BrowserCopilot:
# switch; its presence is a cheap bot tell Turnstile reads.
ignore_default_args=["--enable-automation"],
)
# Hide the "HeadlessChrome" UA only when actually headless; the visible
# window already uses a normal Chrome UA (and that path works today).
if self.headless:
launch_kwargs["user_agent"] = _STEALTH_UA
# Pin the UA on every launch, headless or visible. Both must earn
# cf_clearance under the exact string the curl_cffi driver replays;
# leaving the visible window on Playwright's native UA would bind the
# clearance to a version the driver doesn't present, re-gating chat.
launch_kwargs["user_agent"] = _STEALTH_UA
if self.proxy:
launch_kwargs["proxy"] = self._parse_proxy(self.proxy)
self._context = self._pw.chromium.launch_persistent_context(
+8 -1
View File
@@ -25,6 +25,7 @@ _CURL_SOCKET_BAD = -1
from .challenges import solve_copilot_challenge, solve_hashcash
from .models import AbstractProvider, Conversation, ImageResponse, ImageType
from .protocol import CHAT_WEBSOCKET_URL, CONSENTS_FRAME, SET_OPTIONS_FRAME
from .useragent import CHROME_CLIENT_HINTS, CHROME_UA, IMPERSONATE_TARGET
from .utils import drain_json, is_accepted_format, raise_for_status, to_bytes
@@ -119,7 +120,13 @@ class Copilot(AbstractProvider):
with Session(
timeout=timeout,
proxy=proxy,
impersonate="chrome",
# Pin the TLS/HTTP2 fingerprint, then override the UA + client hints so
# the wire presentation is a fixed Windows Chrome. cf_clearance is bound
# to the earning UA; the browsers that earn it present this same string,
# so the driver must too — otherwise every turn is gated behind a
# Cloudflare Turnstile. See copilot/useragent.py.
impersonate=IMPERSONATE_TARGET,
headers={"User-Agent": CHROME_UA, **CHROME_CLIENT_HINTS},
cookies=cookies,
) as session:
# Establish cookies + Cloudflare clearance (anonymous is fine).
+48
View File
@@ -0,0 +1,48 @@
"""The single User-Agent the whole bridge presents to Cloudflare.
Cloudflare binds ``cf_clearance`` to the *exact* User-Agent string that earned
it. The bridge touches that one cookie from three places — the curl_cffi chat
driver (which *uses* it), the headless refresh, and the interactive login (which
*earn* it) — so all three must present a byte-identical UA or the clearance is
distrusted and the chat socket gates every turn behind a Cloudflare Turnstile.
Keeping the string here (imported by both :mod:`copilot.driver` and
:mod:`copilot.browser`) makes drift impossible.
Why these exact values:
* ``CHROME_UA`` is a real desktop **Windows** Chrome UA. We standardise on the
same major version Playwright actually bundles (see the maintenance note), so
overriding a launched Chromium's UA to this string does *not* contradict the
browser's native ``Sec-CH-UA`` client hint — both say the same version.
* ``IMPERSONATE_TARGET`` pins curl_cffi to a fixed TLS/HTTP2 fingerprint. Left as
the bare ``"chrome"`` alias it tracks curl_cffi's ``DEFAULT_CHROME``, which
advances on every upgrade (and ships a *macOS* UA) — a moving target that
silently re-breaks the UA match. Pin to the closest stable profile instead; the
driver overrides the UA + client hints on top so the wire presentation stays
Windows/``CHROME_UA`` regardless of the profile's native UA.
MAINTENANCE: bump ``CHROME_UA``'s major version whenever ``playwright install``
upgrades the bundled Chromium (check ``chromium.launch().version``). If the
constant lags the real browser, the browser's native ``Sec-CH-UA`` out-drifts the
spoofed UA and Turnstile sees the mismatch. One line, one place.
"""
# Real desktop Windows Chrome. Must match Playwright's bundled Chromium major
# version (currently 148) so the UA override introduces no client-hint conflict.
CHROME_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"
)
# Client hints that must accompany CHROME_UA so the platform/version a server
# reads from the hints agrees with the UA line. Used by the curl_cffi driver,
# which otherwise emits the impersonation profile's native (macOS) hints.
CHROME_CLIENT_HINTS = {
"sec-ch-ua-platform": '"Windows"',
"sec-ch-ua": '"Google Chrome";v="148", "Chromium";v="148", "Not_A Brand";v="24"',
}
# Pinned curl_cffi impersonation profile (TLS/HTTP2 fingerprint). Closest stable
# profile to CHROME_UA's version; the UA itself is overridden on top.
IMPERSONATE_TARGET = "chrome146"