Metadata-Version: 2.4
Name: okhttp-tls
Version: 0.1.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Summary: Python HTTP client (sync + async) that speaks TLS and HTTP/2 like Android OkHttp + Conscrypt, with a Rust core
Keywords: http-client,tls,boringssl,okhttp,android,conscrypt,http2,rust
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# okhttp-tls

[English](#english) | [中文](#中文)

---

## English

A Python HTTP client whose TLS ClientHello and HTTP/2 frames match
**Android OkHttp + Conscrypt** — Rust core, synchronous and asyncio interfaces.

On Android, OkHttp's TLS layer is **Conscrypt, a Java wrapper around BoringSSL**.
This library uses the **same TLS library** (BoringSSL, via the `boring` crate)
configured the same way, so matching the configuration means matching the bytes:
same cipher suites and order, same extension order, same signature algorithms
and groups. Profiles built from device samples are checked field by field
against those captures in CI on macOS, Linux x86_64, Linux arm64 and Windows.

> Not affiliated with Square, Inc. "OkHttp" is the name of Square's HTTP client;
> this library reproduces its wire behaviour and ships no OkHttp code.

### Features

- **OkHttp + Conscrypt TLS shape**: 15 cipher suites in BoringSSL's on-device
  order, ALPN `h2,http/1.1`, OCSP `status_request`, and the post-quantum group
  `X25519MLKEM768` on newer Conscrypt. The cipher order is fixed by the
  profile, not by the host CPU, so an x86 server emits the device shape exactly
- **Real OkHttp HTTP/2 fingerprint**: SETTINGS carries only
  `INITIAL_WINDOW_SIZE=16777216`, connection `WINDOW_UPDATE` of `16711681`,
  no PRIORITY frames, pseudo-header order `:method :path :authority :scheme`,
  first stream id 3, HPACK indexing and Huffman choices as OkHttp makes them
  (Akamai h2 fingerprint `4:16777216|16711681|0|m,p,a,s`)
- **Protocol chosen by ALPN**: h2 when the server selects it, otherwise
  HTTP/1.1; `Response.http_version` tells you which
- **OkHttp header order**: your headers go out in the order you give them;
  engine headers are appended the way OkHttp's `BridgeInterceptor` does —
  Content-Type → Content-Length / Transfer-Encoding → Host → Connection →
  Accept-Encoding → Cookie → User-Agent — and never override ones you set.
  HTTP/1.1 keeps header-name case; HTTP/2 lowercases names (RFC 9113)
- **Client behaviour follows OkHttp 4.12 defaults**:
  - redirects: up to 20; follows 300–303, 307, 308; `Authorization` is
    dropped on cross-host redirects
  - connection pool: at most 5 idle connections, kept for 5 minutes
  - cookies are sent in insertion order; gzip is decoded transparently;
    method names are sent verbatim
  - timeouts: `connect_timeout` / `read_timeout` / `write_timeout` default to
    10 s each (read/write are the longest idle gap between two I/O operations,
    not a total); `timeout` is the overall cap (default 20 s). **A timeout never
    triggers a resend**, so a slow POST is not executed twice
- **TLS session resumption** on by default, as on a real device. Tickets are
  kept per (scheme, host, port, proxy) and never shared across proxies.
  Pass `tls_session_reuse=False` to always send a fresh handshake
- **Proxy support** per client or per request (`http://user:pass@host:port`);
  https targets go through a CONNECT tunnel with preemptive Basic auth
- **Sync + async**: blocking `Client` and asyncio-native `AsyncClient`
  (Rust tokio runtime underneath)
- **Embedded CA store** (Mozilla bundle), **typed** (`.pyi` stub, `py.typed`),
  layered exceptions
- Prebuilt wheels for macOS (Apple Silicon), Linux (x86_64 and aarch64,
  manylinux_2_28) and Windows (x86_64); one wheel per platform covers
  Python ≥ 3.8 (abi3)

### Install

```bash
pip install okhttp-tls
```

### Usage

Synchronous:

```python
import okhttp_tls

with okhttp_tls.Client(impersonate="okhttp_4_12_0", timeout=20.0) as client:
    resp = client.post(
        "https://example.com/api",
        headers=[("x-api-key", "K"), ("content-type", "application/json")],
        content=b'{"q":"x"}',
    )
    print(resp.status_code, resp.http_version, resp.headers.get("server"), resp.json())
```

Asynchronous:

```python
import asyncio
import okhttp_tls

async def main():
    async with okhttp_tls.AsyncClient() as client:
        r1, r2 = await asyncio.gather(
            client.get("https://example.com/a"),
            client.post("https://example.com/b", json={"k": 1}),
        )

asyncio.run(main())
```

Per-request proxy override:

```python
resp = client.get(url, proxy="http://user:pass@host:port")
```

Error handling:

```python
try:
    resp = client.get(url)
except okhttp_tls.ProxyError:
    ...
except okhttp_tls.TimeoutException:
    ...
except okhttp_tls.OkhttpError:   # base class of all library errors
    ...
```

### Profiles

| Short name | Canonical name | Groups | Source |
|---|---|---|---|
| `okhttp_4_12_0` **(default)** | `okhttp_4_12_0_conscrypt_pq` | `X25519MLKEM768, X25519, P-256, P-384` | real device |
| `okhttp_4_11_0` | `okhttp_4_11_0_conscrypt` | `X25519, P-256, P-384` | real device |
| `okhttp_5_3_2` | `okhttp_5_3_2_conscrypt_pq` | `X25519MLKEM768, X25519, P-256, P-384` | inferred |

`impersonate=` accepts either name. The default User-Agent is `okhttp/<version>`.

**Why the canonical name carries a Conscrypt part.** The fingerprint has two
independent dimensions: the OkHttp version decides the cipher list, ALPN and
TLS version range; the **Conscrypt version** decides the groups and extensions.
Conscrypt is an Android mainline module updated through Google Play system
updates, independently of the OS version — the same device on the same Android
release went from no post-quantum group to `X25519MLKEM768` after such an
update. A version number alone is therefore ambiguous. A short name points to
the profile with real-device evidence for that OkHttp version;
`profile_info()["name"]` is always the canonical name.

**`source="inferred"`**: `okhttp_5_3_2` has no separate device sample. The
OkHttp 4.12 and 5.3.2 `ConnectionSpec` cipher lists are identical, so it is
declared an alias of the 4.12 profile (`alias_of`) instead of inventing a new
fingerprint.

```python
>>> okhttp_tls.profiles()
{'okhttp_4_11_0_conscrypt': 'f3e8b92336e2ffa7b31fcfb9a4bbb617',
 'okhttp_4_12_0_conscrypt_pq': 'ce7fd65a5ac2d85d0c89ee0e41a2e8c7',
 'okhttp_5_3_2_conscrypt_pq': 'ce7fd65a5ac2d85d0c89ee0e41a2e8c7'}
>>> okhttp_tls.profile_info("okhttp_4_12_0")["ja4"]
't13d1512h2_8daaf6152771_40271e0a5736'
```

**Canonical vs. on-the-wire JA3/JA4.** `profiles()` and `profile_info()` give
the fingerprint of a fresh connection. A live ClientHello may additionally
carry `pre_shared_key` (41) when a session is resumed or `padding` (21) when
the hello length triggers it, and omits `key_share` / `psk_key_exchange_modes`
/ `supported_versions` (51/45/43) if the server only speaks TLS 1.2. These are
added by BoringSSL based on connection state, exactly as on a device; they
change JA3 and JA4, so compare like with like.

### API

- Constructors (`Client` / `AsyncClient`): `impersonate`, `proxy`,
  `cookie_store` (default off), `timeout` (overall, default 20 s),
  `connect_timeout` / `read_timeout` / `write_timeout` (default 10 s each;
  `0` disables the read/write limit), `follow_redirects` (default on), `max_redirects`
  (default 20), `verify` (default on), `pool` (keep-alive, default on),
  `idle_timeout` (seconds, default 300), `auto_uncompress` (gzip, default on),
  `user_agent`, `tls_session_reuse` (default on)
- Methods: `get / post / put / delete / patch / head / options /
  request(method, ...)`, accepting `headers` (`list[tuple]` keeps order; a
  `dict` also works), `content` (bytes) or `json` (serialized, adds
  `content-type: application/json` if absent), and `proxy`; `close()` and
  `with` / `async with` (`AsyncClient` also has `aclose()`). In asyncio code,
  use `AsyncClient` rather than `Client`
- `Response`: `status_code` (alias `status`), `http_version`
  (`"HTTP/1.1"` / `"HTTP/2"`), `headers`, `raw_headers` (ordered list of
  tuples), `content` (bytes), `text`, `json()`, `url` (final URL after redirects)
- `Headers`: case-insensitive `get(name, default)`, `headers[name]`,
  `name in headers`, `get_all(name)`, `keys()` / `values()` / `items()`;
  iterating and integer indexing yield `(name, value)` tuples
- `profiles()` → `{canonical name: canonical JA3}`;
  `profile_info(name)` → `name`, `short_name`, `ja3`, `ja4`, `http`, `alpn`,
  `source`, `evidence`, `user_agent` (plus `alias_of` for inferred profiles)
- Exceptions, all subclasses of `OkhttpError(RuntimeError)`:
  `BuilderError`, `ConnectError`, `ProxyError` (proxy unreachable, CONNECT
  refused, or `407`), `TLSError`, `TimeoutException`, `NetworkError`,
  `DecodeError`, `ClientClosedError`

The Python API is a superset of the sibling library
[`dartio-tls`](https://pypi.org/project/dartio-tls/) (Flutter `dart:io`
fingerprint): switching between them means changing the module name and the
`impersonate` profile.

### Limitations

- **Android Conscrypt only.** Desktop/server JVM OkHttp uses SunJSSE, not
  BoringSSL, and has a completely different ClientHello; it is not covered
- No HTTP/3 / QUIC, no WebSocket, no Cronet/Chromium shape
- HTTP/2 sends one request per connection (no multiplexing). This does not
  change the TLS or frame fingerprint. Flow control covers what is needed —
  windowed sending and receive-window refill — with no priority tree and no
  server push
- TLS 1.3 cipher suites are BoringSSL's fixed three and cannot be changed
- Plain-http targets reached through a proxy do not send `Proxy-Authorization`
  (only CONNECT for https carries credentials)
- No wheel for Intel macOS yet

### License

MIT

---

## 中文

TLS ClientHello 与 HTTP/2 帧形态对齐 **Android OkHttp + Conscrypt** 的 Python HTTP
客户端——Rust 内核，同步 + asyncio 双面。

Android 上 OkHttp 的 TLS 底座是 **Conscrypt，即 BoringSSL 的 Java 封装**。本库使用
**同一个 TLS 库**（BoringSSL，经 `boring` crate）并按同样方式配置，所以配置对齐即
字节对齐：cipher 套件及顺序、扩展顺序、签名算法、groups 全部一致。来自真机样本的档
都在 CI 里于 macOS、Linux x86_64、Linux arm64、Windows 上与抓包逐字段比对。

> 与 Square, Inc. 无关联。"OkHttp" 是 Square 的 HTTP 客户端项目名；本库复刻其线上
> 行为，不包含任何 OkHttp 代码。

### 特性

- **OkHttp + Conscrypt 的 TLS 形态**：15 个 cipher 套件按 BoringSSL 真机顺序排列、
  ALPN `h2,http/1.1`、OCSP `status_request`，新版 Conscrypt 档带后量子组
  `X25519MLKEM768`。cipher 顺序由档决定、不看宿主 CPU，x86 服务器上也能精确发出
  真机形态
- **真实的 OkHttp HTTP/2 指纹**：SETTINGS 只含 `INITIAL_WINDOW_SIZE=16777216`、
  连接级 `WINDOW_UPDATE` 为 `16711681`、从不发 PRIORITY、伪头序
  `:method :path :authority :scheme`、首流 id 为 3、HPACK 索引与 Huffman 决策与
  OkHttp 一致（Akamai h2 指纹 `4:16777216|16711681|0|m,p,a,s`）
- **协议由 ALPN 协商决定**：服务端选 h2 就走 h2，否则 HTTP/1.1；
  `Response.http_version` 可见
- **OkHttp 头序**：你的请求头按给定顺序原样发出；引擎头按 OkHttp
  `BridgeInterceptor` 的方式追加在后——Content-Type → Content-Length /
  Transfer-Encoding → Host → Connection → Accept-Encoding → Cookie → User-Agent——
  且不覆盖你已给的头。HTTP/1.1 保留头名大小写；HTTP/2 头名全小写（RFC 9113）
- **客户端行为对齐 OkHttp 4.12 缺省值**：
  - 重定向：上限 20 次；跟随 300–303、307、308；跨 host 跳转时剥掉 `Authorization`
  - 连接池：最多 5 条空闲连接，保留 5 分钟
  - Cookie 按种入顺序发送；gzip 透明解压；方法名原样发送
  - 超时：`connect_timeout` / `read_timeout` / `write_timeout` 各缺省 10 秒
    （read/write 是两次读写之间的最长空闲，不是总时长）；`timeout` 为整体上限
    （缺省 20 秒）。**超时不会触发重发**，慢 POST 不会被执行两次
- **TLS 会话复用**默认开启，与真机一致。票据按 (scheme, host, port, 代理) 分开保存，
  不跨代理共用。传 `tls_session_reuse=False` 则每次都是全新握手
- **代理支持**：per-client 或 per-request（`http://user:pass@host:port`）；
  https 目标走 CONNECT 隧道，预先携带 Basic 认证
- **同步 + 异步双面**：阻塞式 `Client` 与 asyncio 原生 `AsyncClient`
  （底层为 Rust tokio runtime）
- **内嵌 CA store**（Mozilla bundle）、**带类型**（`.pyi` 存根与 `py.typed`）、
  分层异常
- 预编译 wheel：macOS（Apple Silicon）、Linux（x86_64 与 aarch64，manylinux_2_28）、
  Windows（x86_64）；每平台一个 wheel 通吃 Python ≥ 3.8（abi3）

### 安装

```bash
pip install okhttp-tls
```

### 用法

同步：

```python
import okhttp_tls

with okhttp_tls.Client(impersonate="okhttp_4_12_0", timeout=20.0) as client:
    resp = client.post(
        "https://example.com/api",
        headers=[("x-api-key", "K"), ("content-type", "application/json")],
        content=b'{"q":"x"}',
    )
    print(resp.status_code, resp.http_version, resp.headers.get("server"), resp.json())
```

异步：

```python
import asyncio
import okhttp_tls

async def main():
    async with okhttp_tls.AsyncClient() as client:
        r1, r2 = await asyncio.gather(
            client.get("https://example.com/a"),
            client.post("https://example.com/b", json={"k": 1}),
        )

asyncio.run(main())
```

per-request 代理覆盖：

```python
resp = client.get(url, proxy="http://user:pass@host:port")
```

异常处理：

```python
try:
    resp = client.get(url)
except okhttp_tls.ProxyError:
    ...
except okhttp_tls.TimeoutException:
    ...
except okhttp_tls.OkhttpError:   # 本库所有异常的基类
    ...
```

### 档

| 短名 | 规范名 | Groups | 来源 |
|---|---|---|---|
| `okhttp_4_12_0` **（缺省）** | `okhttp_4_12_0_conscrypt_pq` | `X25519MLKEM768, X25519, P-256, P-384` | 真机 |
| `okhttp_4_11_0` | `okhttp_4_11_0_conscrypt` | `X25519, P-256, P-384` | 真机 |
| `okhttp_5_3_2` | `okhttp_5_3_2_conscrypt_pq` | `X25519MLKEM768, X25519, P-256, P-384` | 推断 |

`impersonate=` 两种名字都收。缺省 User-Agent 为 `okhttp/<版本>`。

**为什么规范名要带 Conscrypt 维。** 指纹有两个独立维度：OkHttp 版本决定 cipher 列表、
ALPN 与 TLS 版本范围；**Conscrypt 版本**决定 groups 与扩展集。Conscrypt 是 Android
mainline 模块，随 Google Play 系统更新独立升级，与 OS 版本解耦——同一台设备、同一个
Android 版本，经过一次这样的更新就从没有后量子组变成带 `X25519MLKEM768`。所以只写版本号
是有歧义的。短名指向该 OkHttp 版本有真机证据的那一档；`profile_info()["name"]` 恒为规范名。

**`source="inferred"`**：`okhttp_5_3_2` 没有独立的真机样本。OkHttp 4.12 与 5.3.2 的
`ConnectionSpec` cipher 列表逐项相同，因此声明为 4.12 档的别名（`alias_of`），
而不是凭空造一个新指纹。

```python
>>> okhttp_tls.profiles()
{'okhttp_4_11_0_conscrypt': 'f3e8b92336e2ffa7b31fcfb9a4bbb617',
 'okhttp_4_12_0_conscrypt_pq': 'ce7fd65a5ac2d85d0c89ee0e41a2e8c7',
 'okhttp_5_3_2_conscrypt_pq': 'ce7fd65a5ac2d85d0c89ee0e41a2e8c7'}
>>> okhttp_tls.profile_info("okhttp_4_12_0")["ja4"]
't13d1512h2_8daaf6152771_40271e0a5736'
```

**规范形与线上 JA3/JA4。** `profiles()` 与 `profile_info()` 给的是全新连接的指纹。
线上 ClientHello 可能额外带 `pre_shared_key`(41)（会话复用时）或 `padding`(21)
（hello 长度触发时），对端只支持 TLS 1.2 时则没有 `key_share` /
`psk_key_exchange_modes` / `supported_versions`(51/45/43)。这些由 BoringSSL 按连接
状态自动决定，与真机行为一致；它们会改变 JA3 和 JA4，比对时两边要用同一基准。

### API

- 构造（`Client` / `AsyncClient`）：`impersonate`、`proxy`、`cookie_store`（缺省关）、
  `timeout`（整体上限，缺省 20 秒）、`connect_timeout` / `read_timeout` /
  `write_timeout`（各缺省 10 秒；read/write 设 `0` = 不限）、`follow_redirects`（缺省开）、
  `max_redirects`（缺省 20）、`verify`（缺省开）、`pool`（连接复用，缺省开）、
  `idle_timeout`（秒，缺省 300）、`auto_uncompress`（gzip 解压，缺省开）、
  `user_agent`、`tls_session_reuse`（缺省开）
- 方法：`get / post / put / delete / patch / head / options /
  request(任意方法)`，收 `headers`（`list[tuple]` 保序；`dict` 也收）、`content`
  （bytes）或 `json`（自动序列化，未给 content-type 时补
  `content-type: application/json`）、`proxy`；`close()` 与 `with` / `async with`
  （`AsyncClient` 另有 `aclose()`）。asyncio 代码里请用 `AsyncClient` 而不是 `Client`
- `Response`：`status_code`（别名 `status`）、`http_version`（`"HTTP/1.1"` /
  `"HTTP/2"`）、`headers`、`raw_headers`（保序 tuple 列表）、`content`（bytes）、
  `text`、`json()`、`url`（跳转后最终 URL）
- `Headers`：大小写不敏感的 `get(name, default)`、`headers[name]`、`name in headers`、
  `get_all(name)`、`keys()` / `values()` / `items()`；迭代与整数下标得到
  `(name, value)` tuple
- `profiles()` → `{规范名: 规范形 JA3}`；`profile_info(name)` → `name`、
  `short_name`、`ja3`、`ja4`、`http`、`alpn`、`source`、`evidence`、`user_agent`
  （推断档另有 `alias_of`）
- 异常全部继承 `OkhttpError(RuntimeError)`：`BuilderError`、`ConnectError`、
  `ProxyError`（代理连不上、CONNECT 被拒或 `407`）、`TLSError`、`TimeoutException`、
  `NetworkError`、`DecodeError`、`ClientClosedError`

Python 接口是姊妹库 [`dartio-tls`](https://pypi.org/project/dartio-tls/)（Flutter
`dart:io` 指纹）的超集：两者互换只需改模块名与 `impersonate` 档。

### 限制

- **只做 Android Conscrypt 形态。** 桌面/服务端 JVM 上的 OkHttp 走 SunJSSE 而非
  BoringSSL，ClientHello 完全不同，不在覆盖范围内
- 不做 HTTP/3 / QUIC、WebSocket、Cronet/Chromium 形态
- HTTP/2 每条连接只发一个请求（不做多路复用），不影响 TLS 与帧指纹。流控只做必需部分——
  按窗口分片发送、回补接收窗口——不做优先级树，不做服务端推送
- TLS 1.3 的 cipher 套件是 BoringSSL 固定的三个，不可调
- 明文 http 目标经代理时不发 `Proxy-Authorization`（只有 https 的 CONNECT 携带凭据）
- 暂无 Intel macOS 的 wheel

### 许可证

MIT

