Metadata-Version: 2.4
Name: necko-standalone
Version: 0.8.2
Summary: Standalone NSS and Necko HTTP client with a requests-style Python API
License: MPL-2.0
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests<3,>=2.32.5
Requires-Dist: urllib3<3,>=2.2.3
Requires-Dist: cryptography<51,>=45
Requires-Dist: h2<5,>=4.3
Requires-Dist: brotli<2,>=1.2
Requires-Dist: backports.zstd>=1.0; python_version < "3.14"
Requires-Dist: zstandard<1,>=0.23
Requires-Dist: publicsuffixlist<2,>=1.0
Provides-Extra: socks
Requires-Dist: PySocks!=1.5.7,>=1.5.6; extra == "socks"

# Standalone Necko HTTP Library

Version 0.8.2 exposes the Requests 2.x API through native NSS TLS and HTTP/1.1,
HTTP/2, or HTTP/3, without launching Firefox.

## Architecture

The Python package preserves Requests-style preparation, sessions, authentication,
hooks, exceptions, and response objects, with Firefox-oriented network defaults. Its default
HTTP/HTTPS adapters use the standalone C++ socket and HTTP framing APIs.
The chunked decoder is the same `Http1ChunkedDecoder.h` compiled by Firefox.
This remains a partial extraction, not Firefox's complete Necko network stack.

TLS uses our Firefox 159.0 build's NSS 3.131 and NSPR 4.40 over native socket I/O. On this
Windows build NSPR is included in `nss3.dll`. `mozglue.dll`, `softokn3.dll`, and
`freebl3.dll` are packaged alongside it. Python's `ssl` is used only for exception
types; its TLS engine is not used for requests.
HTTP/2 uses hyper-h2, with Firefox 159's SETTINGS, initial flow-control windows,
starting stream ID and pseudo-header order. ALPN advertises `h2,http/1.1` for
origins, and `http/1.1` for HTTPS proxies; selected protocols are actually used.
HTTP/2 streams share a connection and a worker serializes all NSS and protocol
operations. NSS reads and writes are nonblocking after the HTTP/2 handshake;
partial writes retain the pending TLS data and an output queue applies
backpressure. Stream cancellation stays queued without the request deadline,
including cancellation that races with stream creation. HTTP/3 uses the
checkout's Neqo 0.32.0, nss-rs 0.16.1, and neqo-udp
through `necko_quic.dll`. HTTPS Alt-Svc selects HTTP/3; a TCP backup starts after
100 ms if the QUIC handshake is still pending. The first completed authenticated
connection wins, and pending competitors are cancelled. No application request
is sent on the losing connection. As in Firefox 159, if an HTTP/3 connection
closes with a protocol error before any response data arrives, the request is
retried once over TCP with HTTP/3 disabled and without `Alt-Used`; stateless
resets and no-error closes are not retried this way.
urllib3 provides response decompression and retry policy, without making HTTP
requests. Optional SOCKS negotiation uses PySocks over the same native I/O.
No Firefox process, Gecko runtime, libcurl, or urllib3 HTTP transport is required.
The legacy C APIs retain their original behavior, including WinHTTP for HTTPS;
the Python package uses the additive APIs in `necko_stream.h` and `necko_tls.h`.

Certificate chains and hostnames are verified by cryptography's X.509 verifier
against the pinned Firefox NSS server-auth roots or the supplied CA bundle.
The build extracts roots and distrust-after cutoffs from NSS `certdata.txt` and
ships the matching Firefox public suffix list. This is separate from NSS's TLS
record engine and differs from Firefox's mozpkix/CT/OCSP/CRLite policy. Custom
CA stores are isolated, including session resumption; certificates are checked
again after a resumed handshake. Client keys must be unencrypted PEM.

Default requests use Firefox 159 Windows navigation headers, ordering, User-Agent,
Accept-Language, compression and Fetch Metadata. `Session(context="fetch",
document_url="https://example.com/page")` selects script-fetch headers with an
explicit initiating page. Referer follows strict-origin-when-cross-origin.
Explicit headers still override defaults. Context is explicit, not inferred from
previous requests; the library does not execute a page or fetch its subresources.
`Session(user_activation=False)` omits `Sec-Fetch-User` for a navigation without
user activation; its default is `True`.
Cookie handling enforces host-only domains, public suffix rejection, secure
prefixes, and SameSite for the supplied context. Explicit dictionary cookies bind
to the request host. Firefox's partitioned cookie jars are not implemented.

## Install and Use

Install the platform wheel with pip so that Requests and urllib3 dependencies
are resolved. SOCKS proxies additionally require the `socks` extra.

```powershell
python -m pip install .\obj-necko\wheel-v21\necko_standalone-0.8.2-cp314-cp314-win_amd64.whl
```

```python
import necko as requests

with requests.Session() as session:
    session.headers.update({"User-Agent": "my-client/1.0"})
    session.cookies.set("language", "en")
    response = session.post(
        "https://example.com/api",
        params={"page": 1},
        json={"name": "Necko"},
        timeout=(3, 15),
    )
    response.raise_for_status()
    print(response.json())
    print(response.tls)
```

### Choosing the Firefox version

`Session(firefox="115")` selects which Firefox release to imitate. Any release
from 100 to 159 can be chosen; the default is `159`, or the `NECKO_FIREFOX`
environment variable when set. The module helpers take the same argument, for
example `necko.get(url, firefox="128")`. `necko.FIREFOX_VERSIONS` lists the
choices. Sessions with different versions can be used side by side in one
process.

```python
with necko.Session(firefox="115") as session:
    session.get("https://example.com/")
```

The version selects everything that release puts on the wire, as described in
the sections below:

- the TLS ClientHello over TCP, so the JA3 and JA4 fingerprints;
- the request headers: User-Agent (Firefox 110-119 report `rv:109.0`), Accept,
  Accept-Language, Accept-Encoding and Priority;
- the HTTP/2 SETTINGS, PRIORITY frames and stream dependencies, so the Akamai
  HTTP/2 fingerprint;
- HTTP/3: the QUIC ClientHello, QUIC transport parameters, the first packet's
  size, HTTP/3 SETTINGS and the request headers' order;
- `response.tls["profile"]`, for example `firefox-115`;
- whether an HTTP/3 protocol-error close before any response is retried once
  over TCP without `Alt-Used` (159 only).

`response.tls["nss"]` always reports the packaged NSS (3.131): every version is
produced by configuring the same runtime. Session resumption tickets are kept
per session and per version, so a resumed handshake never crosses versions.

### TLS fingerprint (JA3 / JA4) of each version

Firefox changed its ClientHello eight times between 100 and 159. Each version's
settings were read from that release's source and then checked against the
official Windows x64 build of every release 100-159, downloaded from
archive.mozilla.org and captured on the same local listener as necko:

| Firefox | What changed | JA4 | JA3 | JA3n |
| --- | --- | --- | --- | --- |
| 100-118 | | `t13i1714h2_5b57614c22b0_3d5424432f57` | `b8419f4529b976b01d86fb910efac57f` | `fe281b34231900ddc3a502674b54beb5` |
| 119-131 | GREASE ECH sent | `t13i1714h2_5b57614c22b0_5c2c66f702b0` | `a4a7efb11da858ab9c34dc7fbb241bcb` | `87faec9d8efddf2add84582a469ef465` |
| 132-134 | X25519MLKEM768 key share, certificate compression | `t13i1715h2_5b57614c22b0_eeeea6562960` | `670018cbb0006ca9970455b716aa55de` | `212ba05e1c5a6a6b2f9a65c0416e1570` |
| 135-150 | signed certificate timestamps | `t13i1716h2_5b57614c22b0_3cbfd9057e0d` | `3ec5d3c9a10d43b0576e31e639c83cd0` | `92ee0eaa115e93f265dfbc1e869601d6` |
| 151-153 | cipher `0xc009` dropped | `t13i1616h2_86a278354501_3cbfd9057e0d` | `dfc1768fa3cf4a239df894bbfbe05c3d` | `f386735bf4e012a291b036c9371eae1d` |
| 154-155 | cipher `0xc00a` dropped | `t13i1516h2_8daaf6152771_3cbfd9057e0d` | `06260de10ab01f67d90cdbd3fcf216a7` | `1b874cd705a0185b60bcb121ef2e39a2` |
| 156-159 | ffdhe2048 and ffdhe3072 dropped | `t13i1516h2_8daaf6152771_3cbfd9057e0d` | `a6d822a3f8c8542526830987918a61f9` | `a44446f1338fa388aeec118be5b735f0` |

The table shows a connection to an IP address, which carries no SNI (the `i` in
JA4). A host name gives `d` instead, and one more extension: for 159 that is
JA4 `t13d1517h2_8daaf6152771_3cbfd9057e0d`, as reported by
`https://tls.peet.ws/api/all`. Extension order is fixed in every version, so
JA3 is as stable as JA3n.

For 100-118 and 155-159 necko's ClientHello is byte-for-byte that of the real
build. For 119-154 every field and fingerprint matches, but the GREASE ECH
extension is one byte longer (282 instead of 281). Those releases shipped an
older NSS whose ECH padding rounds differently; the packaged NSS 3.131 cannot
produce the old length. JA3 and JA4 do not include extension lengths. The same
applies to the QUIC ClientHello of those releases.

Two real-browser behaviours are not imitated:

- After several failed connections to the same server, Firefox 132 and later
  retry with a reduced ClientHello, without ML-KEM and certificate compression.
  necko always sends the normal ClientHello.
- Firefox 103-105 sometimes added GREASE ECH to one connection of a session,
  although their defaults disable it. necko never does.

### Request headers of each version

Header order is the same in every version: User-Agent, Accept, Accept-Language,
Accept-Encoding, Referer, Origin, Connection, Cookie, Upgrade-Insecure-Requests,
Sec-Fetch-Dest/Mode/Site/User, Priority, and `te: trailers` last over HTTP/2.
The values change:

| Firefox | Accept (navigation) | Accept-Language | Accept-Encoding | Priority (navigation / fetch) |
| --- | --- | --- | --- | --- |
| 100-125 | `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8` | `en-US,en;q=0.5` | `gzip, deflate, br` | none |
| 126-127 | as above | `en-US,en;q=0.5` | `gzip, deflate, br, zstd` | `u=1` / `u=4` |
| 128-131 | adds `image/png,image/svg+xml` after `image/webp` | `en-US,en;q=0.5` | `gzip, deflate, br, zstd` | `u=0, i` / `u=4` |
| 132-146 | `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8` | `en-US,en;q=0.5` | `gzip, deflate, br, zstd` | `u=0, i` / `u=4` |
| 147-159 | as above | `en-US,en;q=0.9` | `gzip, deflate, br, zstd` | `u=0, i` / `u=4` |

Over plain `http://`, Accept-Encoding is `gzip, deflate` in every version.

### HTTP/2 fingerprint of each version

As the Akamai string `SETTINGS|WINDOW_UPDATE|PRIORITY|pseudo-header order`:

| Firefox | Akamai HTTP/2 fingerprint | First stream |
| --- | --- | --- |
| 100-127, 129-131 | `1:65536;4:131072;5:16384\|12517377\|3:0:0:201,5:0:0:101,7:0:0:1,9:0:7:1,11:0:3:1,13:0:0:241\|m,p,a,s` | 15 |
| 128 | `1:65536;4:131072;5:16384;9:1\|12517377\|0\|m,p,a,s` | 3 |
| 132 | `1:65536;2:0;4:131072;5:16384;9:1\|12517377\|0\|m,p,a,s` | 3 |
| 133-159 | `1:65536;2:0;4:131072;5:16384\|12517377\|0\|m,p,a,s` | 3 |

With the PRIORITY tree (the third field), a navigation's HEADERS depends on
stream 13 and a script fetch's on stream 11; without it both depend on 0.
HEADERS weights are 42 for a navigation and 22 for a fetch in every version.

### HTTP/3 of each version

Firefox shipped a different Neqo (QUIC) version almost every release. necko
reproduces each one's handshake with its single Neqo build:

| Firefox | QUIC JA4 | Changes |
| --- | --- | --- |
| 100-105 | `q13d0314h3_55b375c5d22e_2d2a40a25571` | x25519 key share only, FFDHE groups offered; no version_information; transport parameters in random order |
| 106-113 | same | version_information under its draft codepoint |
| 114-118 | same | max_datagram_frame_size 1200 |
| 119-123 | `q13d0314h3_55b375c5d22e_61e396c58b1f` | GREASE ECH |
| 124-129 | same | P-256 key share added, FFDHE dropped; final version_information codepoint; from 125 no Neqo-added Priority header |
| 130-138 | same | first packet 1252 bytes instead of 1357; from 132 no grease_quic_bit or disable_active_migration; from 135 X25519MLKEM768; from 137 SNI split across packets |
| 139-142 | same as 135 | transport parameters in a fixed order |
| 143-154 | `q13d0315h3_55b375c5d22e_dc5437974b47` | certificate compression; from 145 HTTP/3 datagram and CONNECT settings; from 147 max_streams 100 |
| 155-159 | same | QUIC v2 in version_information, reset_stream_at |
| 156 | `q13d0315h3_55b375c5d22e_bb76f32061e3` | as 155, but also offers ML-DSA signatures |

First-packet sizes are for IPv4; over IPv6 each is 20 bytes smaller, as in
Firefox.

To check the fingerprint yourself, disable the system proxy for the session:
when a proxy is used, the server sees the proxy's TLS, not necko's.

```python
with necko.Session(firefox="128") as session:
    session.trust_env = False
    print(session.get("https://tls.peet.ws/api/all").json()["tls"]["ja4"])
```

`tls.peet.ws` sometimes resets the connection mid-response
(`HTTP/2 transport closed: [WinError 10054] ...`). It does that whenever the
client sends a stream `WINDOW_UPDATE`, which Firefox also sends and which is
part of the HTTP/2 fingerprint. Retry the request rather than changing the
frames.

`from necko_requests import Session` remains supported.
Existing Requests exception handlers work because exception classes are shared.
Imports such as `from necko.auth import HTTPDigestAuth` and
`from necko.adapters import HTTPAdapter` are supported.

## Requests Compatibility

The package supports:

- All verb helpers, including OPTIONS, and positional GET params / POST JSON.
- Request, PreparedRequest, Session.prepare_request and Session.send.
- Query parameters, form data, JSON, multipart files, file bodies, and generators.
- RequestsCookieJar, per-request cookies, cookie expiry/domain/path/secure rules.
- Basic, Digest, custom auth, response hooks, and session defaults.
- Redirect history, next requests, method changes, and cross-origin auth stripping.
- Requests exception classes, response truthiness, editable encoding, JSON kwargs,
  reason, elapsed, request metadata, links, and context managers.
- Default unlimited timeout, numeric or (connect, read) timeouts, and urllib3 Timeout.
- Streaming downloads, raw reads, incremental text decoding, gzip and deflate.
- Connection pools, early-close cleanup, stale connection detection, and Retry policies.
- HTTP/HTTPS proxies, CONNECT, proxy authentication, environment proxy settings,
  NO_PROXY and trust_env. SOCKS4/4a/5/5h use the optional PySocks dependency.
- TLS verification, custom CA files/directories, client certificate/key pairs,
  environment CA configuration, and verify=False warnings.
- Session serialization and custom mounted adapters.

Duplicate response headers are available through `response.raw.headers.getlist()`.
The additional `response.headers.get_all()` and `getlist()` helpers are retained.
The optional Session constructor arguments `library_path`, `headers`, `timeout`,
`verify`, `auth`, `context`, `document_url`, and `user_activation` are extensions
to Requests. Protocol-level retries precede the caller's urllib3 retry policy.
The effective Firefox preference limits automatic attempts to ten. Safe methods
or requests on a reused connection can restart after a reset before response
bytes arrive; partial responses and read timeouts do not restart automatically.
REFUSED_STREAM and rejected GOAWAY streams are replayed only with a replayable
body. HTTP_1_1_REQUIRED downgrades the affected transaction. A 421 response on a
coalesced connection retries on a dedicated connection.
HTTP/3 REQUEST_REJECTED retains the alternate route; REQUEST_CANCELLED does not
restart automatically. VERSION_FALLBACK restricts the affected transaction to
HTTP/1.1. Other stream errors use the direct TCP route when the transaction is
restartable. STOP_SENDING with NO_ERROR ends the upload while the response remains
readable; other codes follow the same reset policy. Received response data,
including informational headers, prevents automatic replay.

```python
import necko as requests
from necko.adapters import HTTPAdapter
from urllib3.util import Retry

with requests.Session() as session:
    session.mount(
        "https://",
        HTTPAdapter(
            max_retries=Retry(total=3, status_forcelist=[429, 503]),
        ),
    )
    with session.get(
        "https://example.com/data", stream=True, timeout=(3, 30)
    ) as response:
        response.raise_for_status()
        with open("data.bin", "wb") as output:
            for chunk in response.iter_content(65536):
                output.write(chunk)
```

## Build and Export

A C++17 compiler, Rust 1.97+, Python 3.11+ for building, libclang, the package
dependencies in `pyproject.toml`, and a Firefox 159 Windows x64 NSS SDK are
required. The runtime package supports Python 3.10+. The default SDK location
is `obj-release/dist`; use `--nss-dist` to override it. A matching x64 Visual C++
runtime must be installed on the destination machine.

```sh
python -m pip install "requests>=2.32.5,<3" "urllib3>=2.2.3,<3" "cryptography>=45,<51" "h2>=4.3,<5" "brotli>=1.2,<2" "zstandard>=0.23,<1" "publicsuffixlist>=1.0,<2" PySocks
# Python < 3.14 also needs backports.zstd.
python tools/necko-standalone/build.py --export-nss-sdk obj-necko/nss-sdk-v21
python tools/necko-standalone/build.py --nss-dist obj-necko/nss-sdk-v21 --output obj-necko/nss-v21
python tools/necko-standalone/build.py --nss-dist obj-necko/nss-sdk-v21 --export obj-necko/source-v21
python obj-necko/source-v21/build.py --source-root obj-necko/source-v21 --nss-dist obj-necko/nss-sdk-v21 --output obj-necko/tmp/test-runs/export-build-v21 --work-dir obj-necko/tmp/test-runs/export-build-v21
python -m pip wheel obj-necko/nss-v21 --no-deps --wheel-dir obj-necko/wheel-v21
```

The runtime output includes both native DLLs, Python package, C headers, and SHA-256
artifact manifests. Source exports include provenance hashes and build without
the Firefox checkout. They bundle the Cargo lockfile and its dependency closure,
including optional dependencies required for offline resolution. Export
destinations must be new. `--work-dir` controls temporary object and Cargo output.
CMake builds the C++ core; `build.py` also builds and packages HTTP/3.
The SDK export resolves the checkout's header links into regular files and
retains runtime and configuration-source hashes, so the exported build needs
no Firefox checkout. It contains prebuilt NSS libraries, not an NSS source build.

## Temporary validation workspace

Validation output and cleanup staging use `obj-necko/tmp`. Test logs and
response captures go under `obj-necko/tmp/test-runs`; historical test trees,
generated bytecode, and disposable build state go under
`obj-necko/tmp/delete-queue`. The versioned runtime, SDK, source, and wheel
directories remain outside this temporary workspace. Temporary material is kept
for the user to delete manually; no automatic cleanup is performed. Historical logs and summaries
retain the paths and results from their original runs. Moved virtual environments
are disposable archives; create a fresh environment under `test-runs` for reuse.

From the checkout root, set temporary paths for validation commands:

```powershell
$neckoTmp = (Resolve-Path .\obj-necko\tmp).Path
New-Item -ItemType Directory -Force -Path "$neckoTmp\test-runs\temp" | Out-Null
$env:TEMP = "$neckoTmp\test-runs\temp"
$env:TMP = $env:TEMP
$env:PYTHONPYCACHEPREFIX = "$neckoTmp\test-runs\pycache"
```

## Scope and Upgrade Policy

Windows x64 is the validated build platform. Other platforms require their own
build and integration review before distribution. Native response headers
are limited to 64 KiB, and ambiguous Content-Length / Transfer-Encoding framing
is rejected. Streaming bodies have no fixed 64 MiB limit. DNS resolution uses
the system resolver; its blocking duration is not bounded by connect timeout.

The pinned TLS profile includes TLS 1.2/1.3, Firefox cipher and signature
preferences, ML-KEM-768/X25519 and P-256 key shares, ECH GREASE, OCSP/SCT
extensions, delegated credentials, and working zlib/Brotli/Zstandard certificate
decompression. `response.tls` reports backend/version/cipher/ALPN/resumption.

This is a measurable transport profile, not a complete browser. HTTP/2 and HTTP/3
support concurrent streams and certificate/IP-checked connection coalescing.
HTTP/2 ORIGIN entries accumulate across frames. Valid entries permit coalescing
after certificate verification without another DNS lookup; an initialized set
restricts DNS-based coalescing. Invalid stream/flag contexts are ignored.
Real ECH from DNS HTTPS records, Firefox DNS/cache, partitioned cookies, HSTS
preload and complete browser scheduling are not implemented. HTTP/3 with proxies
or client certificates currently uses TCP instead. HPACK indexing and Huffman encoding follow the
Firefox source and are compared as raw header blocks. 0-RTT is advertised as
Firefox does, but request data waits for the handshake. Redirect plumbing uses
Requests with Firefox header/context recomputation. HTTPS proxy ALPN is limited
to HTTP/1.1. CORS enforcement, DOM events, browser caches, and automatic
navigation/subresource lifecycle require a browser runtime.

On September 24, 2026, a local Firefox 156.0 comparison matched the normalized
ClientHello fields, request headers/order, and HTTP/2 frame payloads for a local
HTTPS GET with equal navigation context. Firefox opened an additional speculative
connection. Four HTTP/1 reset scenarios also matched observed attempt counts
(2, 10, 1, 1). These checks do not establish universal packet, timing, HTTP/3,
resumption, concurrency, or connection-lifecycle equivalence.

Upgrades must move the NSS SDK and Firefox TLS/HTTP2 configuration together,
review the source files listed in `necko/firefox-profile.json`, and verify a clean
wheel installation. The current
profile rejects NSS versions other than 3.131 instead of silently using changed
defaults. Native C ABI additions preserve the existing entry points. MPL-2.0
covers local code and Mozilla components; Python dependencies retain their
upstream licenses. Version 0.8.2 Python modules require the matching native DLL
with the nonblocking I/O entry points; upgrade the package and DLLs together.
