aioweb_tls/README.md
disqualifier ae4c653ecc fix: lock Noble.setup so concurrent first requests fetch the Go-lib once
Noble.setup guarded the one-time Go shared-library fetch with a bare 'if self._updated' flag — a TOCTOU race where concurrent first requests both passed the check before either set the flag, running the download multiple times. now guarded by a per-instance asyncio.Lock with a check-lock-recheck. verified under load: 2/10/100/500 concurrent setups run the fetch exactly once each (a no-lock control runs it N times).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-28 17:46:20 -04:00

174 lines
7.7 KiB
Markdown

# aioweb_tls
TLS-fingerprinting backends for [aioweb](https://git.rethinkstudios.io/rethink-public/aioweb).
One session class — `TLSSession` — takes an **injected backend** that swaps the HTTP
client (and thus the TLS/HTTP fingerprint), while inheriting every aioweb feature —
header overwrites, domain rewriting, ephemeral headers, proxies, retries, previews —
unchanged. The backend swaps the wire, not the session.
```python
from aioweb_tls import TLSSession, CurlCffi, Noble # the two bundled backends
TLSSession(backend=CurlCffi(impersonate="chrome"))
TLSSession(backend=Noble(client="chrome_133"))
TLSSession(backend=MyBackend(...)) # or one you write — see "Writing your own backend"
```
## Install
Depends on `aioweb`. The TLS clients are **optional extras** — install the backend(s)
you want; importing the package never fails because an extra is missing.
`requirements.txt` (pick the extra you need):
```
aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1
aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1
aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1
```
Direct:
```bash
pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1"
pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1"
pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1"
```
- `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both.
- **No extra** (`pip install aioweb_tls`) is also valid: you get `TLSSession` and the
`TLSBackend` protocol — the framework and the bring-your-own-backend path — but no
bundled client. `CurlCffi()` / `Noble()` then raise a clear `RuntimeError` naming the
extra to install. Use a bare install when you only need a custom backend of your own.
Constructing a backend whose client isn't installed raises that `RuntimeError` at
construction, never at import.
## curl_cffi backend
```python
from aioweb_tls import TLSSession, CurlCffi
async with TLSSession(backend=CurlCffi(impersonate="chrome"), proxies={"https": "http://..."}) as s:
resp = await s.request_with_retries("GET", "https://tls.peet.ws/api/all")
if resp: # FailureResponse is falsy
print(resp.json()["tls"]["ja3"])
```
- `CurlCffi(impersonate="chrome")` sets the forged profile; override per call by
passing `impersonate=` to any request method.
- curl_cffi forges JA3/JA4 + HTTP/2 fingerprints via the bundled curl-impersonate binary.
## noble backend
```python
from aioweb_tls import TLSSession, Noble
async with TLSSession(backend=Noble(client="chrome_133")) as s:
await s.setup() # fetch noble's Go shared lib once (network)
resp = await s.request_with_retries("GET", "https://tls.peet.ws/api/all")
if resp:
print(resp.json()["tls"]["ja3"])
```
- `Noble(client="chrome_133")` — accepts a `noble_tls.Client` enum or a string name.
- noble_tls downloads a Go shared library on first use. `await s.setup()` fetches it
once at startup; if you skip it, the first request fetches it lazily. The fetch is
guarded by a lock, so even concurrent first requests download it exactly once.
## Writing your own backend (the `TLSBackend` protocol)
**aioweb_tls ships exactly two backends — `CurlCffi` and `Noble` — plus the
`TLSBackend` protocol for writing your own.** There is no Go, remote, or other
bundled backend; anything beyond curl/noble is something you implement against the
protocol. `TLSSession` itself is backend-agnostic, so a backend you write injects
exactly like the built-ins (`TLSSession(backend=YourBackend(...))`) and inherits
every aioweb feature for free.
To add a backend, write a small object satisfying `TLSBackend` (see `protocol.py`
for the authoritative contract):
| member | required? | signature | purpose |
|---|---|---|---|
| `create_session` | **required** | `(headers, timeout, **kwargs) -> session` | build the client object `TLSSession` stores as `self.session` |
| `raw_request` | **required** | `async (session, method, url, **kwargs) -> aioweb.Response` | send one request; adapt the client's response into an `aioweb.Response` |
| `is_closed` | **required** | `(session) -> bool` | whether the session is closed |
| `cookies_for_url` | optional | `(session, url) -> dict` | cookies for `preview()`; defaults to `{}` |
| `setup` | optional | `async () -> None` | one-time prep (e.g. fetch a native lib); idempotent |
| `close` | optional | `async (session) -> None` | close the session; defaults to `await session.close()` |
`raw_request` receives aioweb-shaped kwargs: the proxy is already resolved into
`kwargs["proxy"]`, headers are merged into `kwargs["headers"]`, and a numeric timeout
is wrapped in an `aiohttp.ClientTimeout` (unwrap `.total`).
### Example — a backend you would write (not included)
The class below is **illustrative template code you implement yourself**, shown to
make the protocol concrete. It is **not** part of aioweb_tls — there is no
`from aioweb_tls import GoTLSBackend`. It sketches one case (talking to a local Go
TLS service over HTTP) so the shape of a conformant backend is clear:
```python
# example: a backend YOU write to drive a local Go TLS service.
# NOT shipped by aioweb_tls — this is a TLSBackend-conformance template.
from aioweb import Response
from aioweb_tls import TLSSession
class GoTLSBackend:
"""example user-written backend: drives a local Go tls-server over HTTP"""
def __init__(self, endpoint: str):
self.endpoint = endpoint
def create_session(self, headers, timeout, **kwargs):
# build/return whatever client object raw_request will use
import aiohttp
return aiohttp.ClientSession(headers=headers)
async def raw_request(self, session, method, url, **kwargs) -> Response:
# kwargs arrive aioweb-shaped: proxy in kwargs["proxy"], headers merged,
# numeric timeout wrapped in an aiohttp.ClientTimeout (unwrap .total).
payload = {"method": method, "url": url, "headers": kwargs.get("headers", {})}
async with session.post(self.endpoint, json=payload) as r:
body = await r.json()
return Response(
status_code=body["status"], headers=body["headers"],
content=body["body"].encode(), url=url, reason=body.get("reason"),
)
def is_closed(self, session) -> bool:
return session.closed
async def close(self, session) -> None:
await session.close()
# inject your backend — same shape as CurlCffi / Noble, all aioweb features inherited
async with TLSSession(backend=GoTLSBackend("http://localhost:8080")) as s:
s.overwrite_domain("internal.local", "127.0.0.1")
resp = await s.request_with_retries("GET", "https://internal.local/x")
```
## Inherited features work unchanged
aioweb's overwrite/domain/ephemeral/proxy/retry/preview logic operates on plain dicts
and never touches the HTTP backend — only the seams do. Every aioweb feature behaves
identically on any backend:
```python
async with TLSSession(backend=CurlCffi(impersonate="chrome")) as s:
s.overwrite_domain("internal.local", "127.0.0.1")
s.set_ephemeral("X-Time", lambda: str(time.time()))
s.overwrite_inject(True)
print(s.preview("GET", "https://internal.local/x").as_curl()) # reflects all of the above
```
## Honesty note
TLS fingerprinting changes one layer — the TLS/HTTP fingerprint. It does **not** by
itself defeat modern bot protection: behavioral analysis, captchas, and JS challenges
are separate signals. Use this as one component, not a complete anti-bot solution.
## Versioning
Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.