Fail fast with a clear error on event loop reuse instead of hanging
A Stealthii instance binds its lock, browser, and contexts to whichever asyncio event loop is running the first time it is used. Using it again from a different loop previously hung forever (the lock/semaphore/ Playwright transport are all bound to the dead first loop and never wake the waiting coroutine). Detected in rsearch's test suite, where pytest-asyncio's default per-test-function event loop broke the module-level Stealthii singleton shared across tests. Now raises StealthUnavailableError immediately with an actionable message. README documents the constraint and the pytest-asyncio config fix (asyncio_default_test_loop_scope = session). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116i7dYLNZTXNR7Lqf439bV
This commit is contained in:
+7
-7
@@ -17,7 +17,7 @@ Applied in `Stealthii._ensure_browser_locked()` at browser launch.
|
||||
| `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. |
|
||||
| `--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
|
||||
|
||||
@@ -42,7 +42,7 @@ and `app` members shaped like the real object's public surface.
|
||||
|
||||
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
|
||||
`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.
|
||||
@@ -55,7 +55,7 @@ A stock headless Chromium reports an empty plugin list and a
|
||||
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
|
||||
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.
|
||||
@@ -71,7 +71,7 @@ already reports plausible values here in most cases.
|
||||
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
|
||||
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`
|
||||
@@ -80,7 +80,7 @@ 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
|
||||
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
|
||||
@@ -94,7 +94,7 @@ Every function or getter installed by the patches above is wrapped in 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-
|
||||
instead of the native-code marker - a check used by several stealth-
|
||||
detection scripts specifically to unmask other stealth patches.
|
||||
|
||||
## Verification
|
||||
@@ -115,7 +115,7 @@ 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
|
||||
- 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()`
|
||||
|
||||
Reference in New Issue
Block a user