Initial version of stealthii
An aiohttp/httpx-shaped async HTTP client backed by a single, persistent, stealth-patched Chromium instance via Playwright. get/post/put/patch/ delete/head/download use context.request (real Chrome TLS/HTTP2 fingerprint, no tab needed); render()/screenshot()/page() escalate to full JS-executing page rendering for targets that need a JS challenge solved. Supports named sessions for cookie persistence/warm-up, and `async with stealth as page` to claim a tab directly, task-safe for concurrent use on one shared instance. Context/browser recycling bounds cookie and on-disk cache growth in a long-lived process. 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,142 @@
|
||||
# stealthii
|
||||
|
||||
`retoor <retoor@molodetz.nl>`
|
||||
|
||||
An `aiohttp`/`httpx`-shaped async HTTP client backed by a single, persistent,
|
||||
stealth-patched Chromium instance. Every request goes out through the real
|
||||
browser's own network stack, so it carries a genuine Chrome TLS/HTTP2/UA
|
||||
fingerprint instead of Python's OpenSSL-based one — the signal most
|
||||
anti-bot systems key on first, before they ever look at a header.
|
||||
|
||||
## Why
|
||||
|
||||
`requests`, `httpx`, and `aiohttp` all negotiate TLS through Python's `ssl`
|
||||
module (OpenSSL). Real browsers negotiate through their own TLS stack
|
||||
(Chrome: BoringSSL). The resulting ClientHello — cipher suites, extensions,
|
||||
curves, and their order — is a fingerprint (JA3/JA4) that anti-bot systems
|
||||
read directly off the handshake, independent of whatever `User-Agent`
|
||||
header the client claims. A Python client cannot fake this from the
|
||||
application layer; the only way to get a real Chrome fingerprint is to
|
||||
make the request from an actual Chrome process.
|
||||
|
||||
Playwright's `BrowserContext.request` API does exactly that: it issues
|
||||
plain HTTP requests through the same browser process backing a real page,
|
||||
without needing a tab or JS execution — as fast as a normal HTTP call, but
|
||||
with the browser's authentic identity. `stealthii` wraps that fast path
|
||||
(`get`/`post`/`put`/`patch`/`delete`/`head`/`download`) behind a familiar
|
||||
API, and adds full page rendering (`render`/`screenshot`/`page`) as a
|
||||
heavier fallback for targets that require solving a JS challenge.
|
||||
|
||||
On top of that, `stealthii` removes the JS-level automation tells a stock
|
||||
headless Chromium exposes (`navigator.webdriver`, missing `window.chrome`,
|
||||
an empty plugin list, a SwiftShader WebGL renderer, a permissions-query
|
||||
mismatch) — verified clean against `bot.sannysoft.com`'s full check suite.
|
||||
|
||||
## What it does not do
|
||||
|
||||
TLS/HTTP2 impersonation defeats fingerprint-based detection. It does not
|
||||
defeat IP-reputation-based rate limiting, nor the behavioural layer of a
|
||||
system like DataDome or a Cloudflare managed challenge — those need a
|
||||
clean IP and, for the hardest targets, genuine human-like interaction.
|
||||
Layer `stealthii` with residential proxies and session warm-up
|
||||
(`session=`, see below) for those targets; don't expect it alone to be a
|
||||
universal bypass.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from stealthii import Stealthii
|
||||
|
||||
stealth = Stealthii() # cheap, lazy — launches nothing yet
|
||||
resp = await stealth.get("https://example.com/api", params={"q": "x"})
|
||||
resp.status
|
||||
await resp.json()
|
||||
await resp.text()
|
||||
|
||||
await stealth.shutdown() # call once at host process shutdown
|
||||
```
|
||||
|
||||
`get`/`post`/`put`/`patch`/`delete`/`head` accept the same shapes as
|
||||
`aiohttp`/`httpx`: `params`, `headers`, `data` (dict → form-urlencoded,
|
||||
bytes/str → raw body), `json`, `files` (multipart), `cookies` (per-call,
|
||||
layered on the context's own jar), `auth` as an `(user, password)` Basic
|
||||
tuple, `allow_redirects` / `follow_redirects` (both spellings accepted),
|
||||
`max_redirects`, and `timeout`.
|
||||
|
||||
```python
|
||||
data = await stealth.download("https://example.com/photo.png") # raw bytes
|
||||
|
||||
png_b64 = await stealth.screenshot("https://example.com") # base64 PNG
|
||||
await stealth.screenshot("https://example.com", path="shot.png") # saved to disk
|
||||
|
||||
rendered = await stealth.render("https://example.com", collect_links=True)
|
||||
rendered["text"]; rendered["links"]; rendered["status"]
|
||||
```
|
||||
|
||||
### Named sessions (cookie persistence / warm-up)
|
||||
|
||||
Every call is anonymous by default (a shared, periodically recycled
|
||||
context). Pass `session="name"` to use a dedicated, reused context instead
|
||||
— cookies set by one call under that name are seen by the next:
|
||||
|
||||
```python
|
||||
await stealth.render("https://example.com/", session="acme") # picks up cookies
|
||||
resp = await stealth.get("https://example.com/api", session="acme") # reuses them
|
||||
```
|
||||
|
||||
### Raw page access
|
||||
|
||||
For anything the high-level API doesn't cover, `page()` yields a real
|
||||
Playwright `Page` (already stealth-patched), guaranteed to close even on
|
||||
exception:
|
||||
|
||||
```python
|
||||
async with stealth.page() as pg:
|
||||
await pg.goto("https://example.com")
|
||||
await pg.click("#accept-cookies")
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is passed once, at construction — `Stealthii(...)` does
|
||||
no I/O itself; the browser launches lazily on first real use:
|
||||
|
||||
```python
|
||||
stealth = Stealthii(
|
||||
max_tabs=4, # concurrent render()/page() tabs
|
||||
fingerprint="ja4", # "ja3" or "ja4" — see fingerprint_info()
|
||||
request_timeout=15.0,
|
||||
max_retries=3,
|
||||
disk_cache_bytes=1, # keep Chromium's own on-disk cache tiny
|
||||
recycle_context_after=(500, 1800.0), # (max requests, max seconds) per context
|
||||
recycle_browser_after_seconds=6*3600, # full relaunch cadence, or None to disable
|
||||
)
|
||||
```
|
||||
|
||||
`fingerprint` selects which hash `fingerprint_info()` reports (JA3 or
|
||||
JA4) — it does not change what's sent on the wire; both describe the same
|
||||
genuine Chromium handshake.
|
||||
|
||||
### Why the recycle settings exist
|
||||
|
||||
A browser that runs for the process's entire lifetime otherwise
|
||||
accumulates cookies and an on-disk HTTP cache without bound. `stealthii`
|
||||
closes and recreates any context — anonymous or named — once it crosses
|
||||
`recycle_context_after`, and fully relaunches the browser (discarding its
|
||||
whole profile directory) every `recycle_browser_after_seconds`. Named
|
||||
sessions meant to stay warm longer should get a larger
|
||||
`recycle_context_after` at construction time.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
make test-unit # no network required
|
||||
make test-integration # hits real sites; requires Chromium installed
|
||||
```
|
||||
Reference in New Issue
Block a user