Add PUT/PATCH/DELETE/HEAD, full aiohttp/httpx param parity, screenshots,
async-with tab claiming, and full documentation - get/post/put/patch/delete/head now accept the same request shape as aiohttp/httpx: data/json/files/cookies/auth/allow_redirects/ follow_redirects/max_redirects, plus the existing session= for named cookie-persisting contexts. - screenshot(url, path=None) captures a PNG, saved to disk or returned as base64. - `async with stealth as page` claims a tab directly, task-safe for concurrent use on one shared instance (keyed off asyncio.current_task, not instance state). - Strip all docstrings/comments from the source (author line excepted), per house style. - README.md rewritten as the complete documentation: rationale, JA3/JA4 comparison, dependencies, construction/laziness/lifetime, every method with examples, named sessions, configuration, error handling, testing. - STEALTH_PATCHES.md: a literal, patch-by-patch account of every launch argument and JS patch applied, with rationale and verification method. - 20 new tests: full HTTP-method coverage, cookies, auth, multipart, download, render, screenshot, named-session isolation, retry/error paths, and guaranteed page/context cleanup under exception. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116i7dYLNZTXNR7Lqf439bV
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# Stealth Patches
|
||||
|
||||
`retoor <retoor@molodetz.nl>`
|
||||
|
||||
This document enumerates every modification stealthii applies to the
|
||||
underlying Chromium/Playwright stack and the rationale for each. It is
|
||||
maintained as living documentation: any change to `stealth_js.py` or the
|
||||
launch arguments in `client.py` must be reflected here in the same commit.
|
||||
|
||||
## Launch-level patches
|
||||
|
||||
Applied in `Stealthii._ensure_browser_locked()` at browser launch.
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `--disable-blink-features=AutomationControlled` | Removes the `navigator.webdriver` flag and associated automation-only code paths at the engine level, before any JS patch runs. |
|
||||
| `ignore_default_args=['--enable-automation']` | Playwright passes `--enable-automation` to Chromium by default, which enables the automation infobar and related detectable behaviour. Suppressed. |
|
||||
| `--no-sandbox` | Required to run headless Chromium as a non-root, unprivileged process in this deployment environment. Not a stealth measure. |
|
||||
| `--disable-dev-shm-usage` | Avoids `/dev/shm` exhaustion under containerized/limited-memory environments. Not a stealth measure. |
|
||||
| `--disk-cache-size=<bytes>` / `--media-cache-size=<bytes>` | Bounds Chromium's own on-disk HTTP/media cache, configurable via `disk_cache_bytes` (default near-zero). Not a stealth measure — an operational safeguard against unbounded disk growth in a long-lived process. |
|
||||
|
||||
## JS-level patches
|
||||
|
||||
Applied via `context.add_init_script(STEALTH_INIT_JS)` on every context
|
||||
before any page script runs, so the patched state is present from the very
|
||||
first document, including cross-origin iframes.
|
||||
|
||||
### `navigator.webdriver`
|
||||
|
||||
Redefined to return `undefined`. This flag is `true` on any
|
||||
CDP-automated Chromium and is the single most widely checked automation
|
||||
signal.
|
||||
|
||||
### `window.chrome`
|
||||
|
||||
A stock headless Chromium launched via Playwright does not expose
|
||||
`window.chrome` at all, unlike a real Chrome install. When absent, a
|
||||
`window.chrome` object is constructed with `runtime`, `loadTimes`, `csi`,
|
||||
and `app` members shaped like the real object's public surface.
|
||||
|
||||
### `navigator.permissions.query`
|
||||
|
||||
A stock headless Chromium reports `denied` for a `notifications`
|
||||
permission query while `Notification.permission` itself still reports
|
||||
`default` — a mismatch a real browser never produces. The query is
|
||||
patched to always return the same value as `Notification.permission`
|
||||
for that one permission name; all other permission names are passed
|
||||
through to the original implementation unmodified.
|
||||
|
||||
### `navigator.plugins` / `navigator.mimeTypes`
|
||||
|
||||
A stock headless Chromium reports an empty plugin list and a
|
||||
`navigator.plugins` value that is a plain array rather than a
|
||||
`PluginArray`. Five entries matching Chrome's real built-in PDF-plugin
|
||||
set (`PDF Viewer`, `Chrome PDF Viewer`, `Chromium PDF Viewer`,
|
||||
`Microsoft Edge PDF Viewer`, `WebKit built-in PDF`) are constructed, each
|
||||
individually reassigned the real `Plugin.prototype`, and the containing
|
||||
array reassigned `PluginArray.prototype` — including
|
||||
`Symbol.toStringTag`, so both a property count check and a
|
||||
`Object.prototype.toString.call(...)` type check pass. `navigator.mimeTypes`
|
||||
is patched the same way for the corresponding `application/pdf` MIME type.
|
||||
|
||||
### `navigator.deviceMemory`, `navigator.hardwareConcurrency`
|
||||
|
||||
Set to `8` each. Present mainly for consistency with the spoofed WebGL
|
||||
GPU (below) and a realistic desktop profile; a stock headless Chromium
|
||||
already reports plausible values here in most cases.
|
||||
|
||||
### `WebGLRenderingContext.prototype.getParameter` / `WebGL2RenderingContext.prototype.getParameter`
|
||||
|
||||
A stock headless Chromium renders WebGL through SwiftShader (a software
|
||||
rasterizer), which reports `UNMASKED_VENDOR_WEBGL` (constant `37445`) as
|
||||
`Google Inc. (Google)` and `UNMASKED_RENDERER_WEBGL` (constant `37446`)
|
||||
containing the literal string `SwiftShader` — an unambiguous headless
|
||||
signal checked by essentially every fingerprinting script. Both
|
||||
constants are intercepted and rewritten to report a real discrete GPU
|
||||
(`Google Inc. (NVIDIA)` / an ANGLE-wrapped `NVIDIA GeForce RTX 3060`
|
||||
string); every other parameter is passed through unmodified.
|
||||
|
||||
### `HTMLIFrameElement.prototype.contentWindow`
|
||||
|
||||
A known secondary check reads `window.chrome` from inside a freshly
|
||||
created, same-origin iframe rather than the top-level document — since
|
||||
each document gets its own JS globals, an iframe's `window.chrome` can
|
||||
be absent even when the top-level one was patched. The `contentWindow`
|
||||
getter is wrapped so that, whenever the returned window is missing
|
||||
`chrome`, it is assigned from the outer patched value before being
|
||||
returned.
|
||||
|
||||
### `Function.prototype.toString` spoofing on every patch above
|
||||
|
||||
Every function or getter installed by the patches above is wrapped in a
|
||||
`Proxy` whose own `toString` is overridden to return the exact string a
|
||||
real native function produces (`function name() { [native code] }`).
|
||||
Without this, a page can distinguish a patched native method from a real
|
||||
one by calling `.toString()` on it and observing actual JavaScript source
|
||||
instead of the native-code marker — a check used by several stealth-
|
||||
detection scripts specifically to unmask other stealth patches.
|
||||
|
||||
## Verification
|
||||
|
||||
Patches are verified against
|
||||
[bot.sannysoft.com](https://bot.sannysoft.com)'s full detection table
|
||||
(`tests/test_stealth_detection.py::test_sannysoft_detection_suite_is_clean`),
|
||||
run on every change. As of the current `STEALTH_INIT_JS`, every row on
|
||||
that page passes, including `WebDriver (New)`, `Chrome (New)`,
|
||||
`Plugins is of type PluginArray`, `WebGL Vendor`/`WebGL Renderer`, and
|
||||
the `HEADCHR_*`/`PHANTOM_*`/`SELENIUM_DRIVER` probe blocks.
|
||||
|
||||
The `get`/`post`/`put`/`patch`/`delete`/`head`/`download` fast path (no
|
||||
tab, `context.request`) is separately verified
|
||||
(`test_fast_path_uses_real_chromium_fingerprint`) to carry a JA3/JA4
|
||||
hash distinct from a plain Python `aiohttp` client, confirming it uses
|
||||
the browser's own TLS stack rather than Python's.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The `context.request` fast path negotiates HTTP/1.1, not HTTP/2 — a
|
||||
real Chrome page load always uses HTTP/2 against an HTTP/2-capable
|
||||
server. A system that cross-checks the TLS fingerprint against the
|
||||
negotiated protocol can detect this specific inconsistency. `render()`
|
||||
(full page navigation) does not have this limitation.
|
||||
- None of the above defeats IP-reputation-based rate limiting, nor the
|
||||
behavioural layer of systems such as DataDome or a Cloudflare managed
|
||||
challenge. Those require a clean source IP and, for the hardest
|
||||
targets, human-like interaction; stealthii's patches remove
|
||||
fingerprint-level tells, not behavioural ones.
|
||||
- Verified against Chromium's stock headless build as shipped with
|
||||
Playwright at the time of writing. A Chromium upgrade can change
|
||||
default values the patches assume (plugin list shape, WebGL constants,
|
||||
permission defaults); re-run the sannysoft regression test after any
|
||||
Playwright/Chromium version bump.
|
||||
Reference in New Issue
Block a user