Metadata-Version: 2.4
Name: rex-tls
Version: 2.1.0
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Dist: certifi>=2024
Requires-Dist: maturin>=1.9,<2 ; extra == 'dev'
Requires-Dist: pytest>=8,<10 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24,<2 ; extra == 'dev'
Requires-Dist: requests>=2.32,<3 ; extra == 'dev'
Requires-Dist: trustme>=1.2.1,<2 ; extra == 'dev'
Requires-Dist: websocket-client>=1.8,<2 ; extra == 'dev'
Requires-Dist: h2>=4.3,<4.4 ; python_full_version < '3.10' and extra == 'dev'
Requires-Dist: h2>=4.4,<5 ; python_full_version >= '3.10' and extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: Python HTTP client with native Android Chrome and OkHttp TLS/HTTP2 profiles
Keywords: tls,http2,android,chrome,okhttp,boringssl
Home-Page: https://github.com/rex3129909440/rex-tls
Author: rex-tls contributors
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/rex3129909440/rex-tls#readme
Project-URL: Homepage, https://github.com/rex3129909440/rex-tls
Project-URL: Repository, https://github.com/rex3129909440/rex-tls

# rex-tls

`rex-tls` 是一个面向 Python 的原生移动端 TLS/HTTP 客户端。它提供接近
`requests` 的调用方式，并在 Rust 原生扩展中实现 TLS、HTTP/1.1、HTTP/2
以及 Chrome Android 的 HTTP/3 网络路径。

项目使用 vendored BoringSSL，不依赖 `wreq`、`reqwest`、`curl` 或系统
OpenSSL。当前发布版是
[rex-tls 2.1.0](https://pypi.org/project/rex-tls/2.1.0/)；仅提供预编译二进制 wheel，不发布 sdist。

> 适用范围：协议兼容测试、移动客户端互操作、经过授权的网络研究和普通
> HTTP 客户端开发。本项目不包含验证码求解、WAF 绕过或站点专用规避逻辑。

## 支持的 profile

| profile | 协议 | 设备验证基线 |
|---|---|---|
| `chrome_android_149` | TLS + HTTP/1.1 + HTTP/2 + HTTP/3 | Pixel 4 / Android 11 / Chrome 149.0.7827.200 |
| `chrome_android_150` | TLS + HTTP/1.1 + HTTP/2 + HTTP/3 | Pixel 4 / Android 11 / Chrome 150.0.7871.63 |
| `okhttp_4.12` | TLS + HTTP/1.1 + HTTP/2 | Pixel 4 / Android 11 / OkHttp 4.12.0 |
| `okhttp_5.4` | TLS + HTTP/1.1 + HTTP/2 | Pixel 4 / Android 11 / OkHttp Android 5.4.0 |

四套 profile 均已完成真实设备行为采集，并已在同一个本机候选原生二进制上完成严格验收。
Chrome 149/150 覆盖 TLS、HTTP/2、HTTP/3 wire 与会话行为；OkHttp 4.12/5.4 覆盖冷连接、
TLS 恢复、顺序/并发 H2、POST 和大请求头。两个 OkHttp profile 均不声明 HTTP/3 能力。

## 安装

```bash
python -m pip install rex-tls==2.1.0
```

要求 Python 3.9 或更高版本。当前稳定版提供以下预编译 wheel：

- Windows x86-64
- Linux manylinux 2.28 x86-64

2.1.0 暂不发布 macOS wheel，以控制 CI 成本；macOS 上 pip 不会回退到源码构建。

首选导入名是 `rex_tls`；`mobile_tls` 仅作为兼容入口保留。

## 快速开始

```python
import rex_tls

response = rex_tls.get(
    "https://example.com/",
    profile="chrome_android_149",
    params={"page": 1},
    timeout=30,
)

response.raise_for_status()
print(response.status_code)       # 200
print(response.http_version)      # HTTP/1.1、HTTP/2 或 HTTP/3
print(response.text)
```

发送 JSON：

```python
import rex_tls

response = rex_tls.post(
    "https://api.example.com/items",
    profile="okhttp_4.12",
    json={"name": "测试", "enabled": True},
)
print(response.json())
```

## 推荐：复用 Session

`Session` 会复用连接、Cookie 和 TLS 会话，连续请求时应优先使用：

```python
from rex_tls import Session

with Session(
    "chrome_android_150",
    timeout=20,
    follow_redirects=True,
    max_redirects=10,
) as session:
    session.headers.update({"x-client-id": "demo"})

    first = session.get("https://example.com/start")
    second = session.get("https://example.com/next")

    print(first.connection_reused)
    print(second.connection_reused)
```

## 请求参数与请求体

```python
from rex_tls import Session

with Session("okhttp_4.12") as session:
    # 查询参数
    r1 = session.get(
        "https://api.example.com/search",
        params={"q": "中文", "page": 2},
    )

    # application/x-www-form-urlencoded
    r2 = session.post(
        "https://api.example.com/form",
        data={"username": "demo", "remember": "1"},
    )

    # application/json; charset=utf-8
    r3 = session.post(
        "https://api.example.com/json",
        json={"message": "你好"},
    )

    # 原始内容
    r4 = session.put(
        "https://api.example.com/raw",
        content=b"raw bytes",
        headers={"content-type": "application/octet-stream"},
    )
```

`content`、`data` 和 `json` 三者互斥。支持 `get`、`options`、`head`、
`post`、`put`、`patch`、`delete` 以及通用 `request`。

## 请求头顺序

底层在 ALPN 确定最终协议后，才选择 H1、H2 或 H3 的请求头策略：

- 合并后的用户请求头为空时，使用该 profile、协议和请求上下文的完整默认头；
- 合并后的用户请求头非空时，只排序用户提供的字段，不注入 profile 默认头；
- Chrome 已知字段进入实机分类槽位；未知或自定义字段保持彼此之间的调用者顺序；
- OkHttp 的应用/拦截器决定普通字段顺序，因此保留调用者顺序；
- 需要重复同名头时，可传入元组列表；
- Session 级请求头会先与单次请求头进行大小写不敏感合并；
- `Host`/`:authority`、H2/H3 伪头及请求体所需的 `Content-Length` 属于协议元数据，
  由底层生成，不属于默认头注入。

```python
from rex_tls import Session

with Session("chrome_android_149") as session:
    session.headers.update({"accept-language": "zh-CN,zh;q=0.9"})
    response = session.get(
        "https://example.com/",
        headers=[
            ("user-agent", "My-Mobile-Client/1.0"),
            ("accept", "application/json"),
            ("x-trace-a", "1"),
            ("x-trace-b", "2"),
        ],
    )
```

如果需要 profile 的整套默认头，请不要传 `headers`，也不要修改 `Session.headers`。一旦传入
任意用户头，该集合就被视为完整的用户普通头集合；例如只传 `Accept` 时不会再自动增加
`User-Agent` 或 `Accept-Language`。

该判定早于 CookieJar 和 `json`/`data` 自动生成的 `Content-Type`：只传 `cookies=` 或请求体
仍会保留完整 profile 默认头，再把这些派生字段放入对应协议的排序位置。

H1 字段名会使用实机观察到的大小写（例如 `Host`、`Accept-Encoding`，而 `sec-ch-ua`
保持小写）。HTTP 字段名在语义上大小写不敏感；H2/H3 则按协议要求只能发送小写字段名。
H2/H3 中 `Host` 会转换为 `:authority`，`Connection`、`Keep-Alive` 等逐跳头是非法字段，
库会明确报错而不会静默删除。排序表覆盖内置头和常见标准请求字段；任意 `X-*` 名称空间
无限，无法由有限静态表枚举，因此未知字段采用稳定回退规则。

Chrome 的文档导航上下文可显式控制。下例对应“跨站、无用户激活”的主导航，请求头中的
`sec-fetch-site` 会是 `cross-site`，并省略 `sec-fetch-user`：

```python
with Session("chrome_android_149") as session:
    response = session.get(
        "https://example.com/",
        navigation_site="cross-site",
        user_activation=False,
    )
```

`navigation_site` 可取 `none`、`same-origin`、`same-site`、`cross-site`；这两个参数只适用于
Chrome profile，并会沿同一次原生重定向链传播。库复现显式请求本身，不额外创建浏览器进程
可能产生的预连接或 favicon 请求。

## HTTP 代理

代理映射与 `requests` 风格相近：

```python
from rex_tls import Session

proxies = {
    "http": "http://user:password@proxy.example:8080",
    "https": "http://user:password@proxy.example:8080",
    "no_proxy": ".internal.example,127.0.0.1",
}

with Session("okhttp_4.12", proxies=proxies) as session:
    response = session.get("https://example.com/")
```

也可在单次请求覆盖代理：

```python
response = session.get(
    "https://example.com/",
    proxies={"https": "http://other-proxy.example:3128"},
)

# 显式为这一次请求禁用 HTTPS 代理
direct = session.get("https://example.com/", proxies={"https": None})
```

从环境变量读取 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`：

```python
with Session("chrome_android_149", trust_env=True) as session:
    response = session.get("https://example.com/")
```

当前代理能力边界：

- 支持 `http://` forward proxy 和 HTTPS 目标的 HTTP CONNECT；
- 支持代理 URL 中的 Basic 用户名/密码；
- 不支持 `https://` proxy、SOCKS、PAC、NTLM/Digest 或 MASQUE；
- 代理失败不会静默改成直连；
- 不要把 `Proxy-Authorization` 放入源站请求头，凭据应写在代理 URL 中；
- 传统 HTTP CONNECT 不能承载 QUIC，因此代理场景不使用 HTTP/3。

## 强制 HTTP/2

需要服务端实际协商 H2 时使用 `http2=True`：

```python
from rex_tls import Session

with Session("chrome_android_149", http2=True) as session:
    response = session.get("https://example.com/")
    assert response.http_version == "HTTP/2"
```

该选项保留 profile 已采集的 `h2,http/1.1` ALPN ClientHello 向量，在握手后验证服务端确实
选择 `h2`；这避免为了“强制”而改变 TLS 指纹。若服务端选择 H1、URL 是明文 HTTP（当前
不支持 h2c），请求会明确失败。`http2=True` 与 `http3="auto"`/`"only"` 互斥。强制 H2
时不要提供 `Connection` 等 H1 专用头。

顶层 `rex_tls.get(..., http2=True)`、`AsyncSession`、`SessionPool` 和
`AsyncSessionPool` 同样支持这个构造选项。

## HTTP/3

HTTP/3 只适用于两个显式 Chrome profile，并且只支持 HTTPS URL：

```python
from rex_tls import Session

# 优先尝试 HTTP/3，并允许按协议策略回退到 H2/H1
with Session("chrome_android_149", http3="auto") as session:
    response = session.get("https://example.com/")

# 只允许 HTTP/3；无法建立 H3 时直接报错
with Session("chrome_android_150", http3="only") as session:
    response = session.get("https://example.com/")
```

`http3` 可取 `"off"`、`"auto"`、`"only"`，默认是 `"off"`。使用 H3
时必须写完整的 `chrome_android_149` 或 `chrome_android_150`，不要使用别名。
`okhttp_4.12` 与 `okhttp_5.4` 均不支持 H3。`okhttp` 和 `okhttp_latest` 当前解析为
`okhttp_5.4`；需要长期复现时应固定完整 profile 名称。

## 异步接口

```python
import asyncio
from rex_tls import AsyncSession

async def main() -> None:
    async with AsyncSession("okhttp_4.12") as session:
        response = await session.get("https://example.com/", timeout=15)
        response.raise_for_status()
        print(response.status_code, response.http_version)

asyncio.run(main())
```

取消 asyncio Task 时，客户端会尝试取消对应的原生请求。

## 内置并发池

`SessionPool` 面向多线程，`AsyncSessionPool` 面向 asyncio。每个池成员拥有独立的
TLS/HTTP 连接状态，因此最多可以同时运行 `max_connections` 个请求；超过上限的请求会
排队。普通 `Session` 本身可由多线程并发调用，`AsyncSession(max_concurrency=8)` 默认用
有界 worker 执行并发请求；同 route 的 HTTP/2 请求可复用一条 TCP/TLS 连接上的多个
stream。HTTP/1.1 和当前 HTTP/3 驱动仍按单连接串行。

```python
import asyncio
from rex_tls import AsyncSessionPool

async def main() -> None:
    async with AsyncSessionPool(
        "chrome_android_150",
        max_connections=16,
        session_mode="shared",
        proxy="http://user:password@proxy.example:8080",
    ) as pool:
        pool.headers["x-client"] = "pool-demo"
        responses = await asyncio.gather(
            *(pool.get(f"https://example.com/?id={index}") for index in range(100))
        )
        print([response.status_code for response in responses])

asyncio.run(main())
```

池的可变会话状态有两种模式：

- `session_mode="shared"`（默认）：所有连接共享默认 Header、代理配置和线程安全
  CookieJar，整个池表现为一个登录会话；
- `session_mode="isolated"`：每个连接的 Header、代理配置和 Cookie 均完全独立，适合
  多账号或任务隔离。需要连续使用同一身份时，用
  `async with pool.acquire() as session:` 固定租用一个成员。

```python
async with AsyncSessionPool(
    "okhttp_4.12", max_connections=8, session_mode="isolated"
) as pool:
    async with pool.acquire() as session:
        session.headers["authorization"] = "Bearer account-one"
        session.proxies["https"] = "http://account-one.proxy:8080"
        session.cookies.set("account", "one")
        first = await session.get("https://example.com/first")
        second = await session.get("https://example.com/second")
```

同步代码可以在线程中共享一个池：

```python
from concurrent.futures import ThreadPoolExecutor
from rex_tls import SessionPool

with SessionPool("okhttp_4.12", max_connections=8) as pool:
    with ThreadPoolExecutor(max_workers=32) as executor:
        responses = list(executor.map(pool.get, urls))
```

`pool_timeout=` 只控制等待空闲池成员的时间；`timeout=` 仍控制网络请求。池级
`headers`、`proxies` 和 `trust_env` 会应用到所有成员。

## Response 对象

常用属性和方法：

```python
response.status_code       # int
response.reason            # str
response.url               # 最终 URL
response.http_version      # HTTP/1.1、HTTP/2 或 HTTP/3
response.headers           # 大小写不敏感的响应头映射
response.headers.raw       # 保留顺序及重复项的 tuple
response.headers.get_all("set-cookie")
response.content           # bytes
response.text              # 按 charset 解码后的 str
response.json()            # JSON 解码
response.ok                # status_code < 400
response.elapsed_seconds
response.local_address
response.remote_address
response.connection_reused
response.tls_session_reused
response.raise_for_status()
```

## TLS 证书验证

默认使用 `certifi` CA 包并验证证书与主机名：

```python
from rex_tls import Session

# 默认验证
session = Session("chrome_android_149", verify=True)

# 使用自定义 CA bundle
session = Session("chrome_android_149", verify="/path/to/ca-bundle.pem")

# 仅用于受控测试环境
session = Session("chrome_android_149", verify=False)
```

## 异常处理

```python
import rex_tls

try:
    response = rex_tls.get(
        "https://example.com/",
        profile="chrome_android_149",
        timeout=10,
    )
    response.raise_for_status()
except rex_tls.RequestError as exc:
    print(f"请求失败: {exc}")
except rex_tls.MobileTLSError as exc:
    print(f"原生客户端错误: {exc}")
```

## 查询运行时能力

```python
import rex_tls

print(rex_tls.profiles())
print(rex_tls.profile_info("chrome_android_149"))
print(dict(rex_tls.native_versions()))
```

## 与 requests 的差异

`rex-tls` 对齐的是常用调用方式，不是 `requests` 的完全兼容替代：

- 当前没有公开的 `auth`、`hooks`、`adapters` 和 `mount`；
- `Session.cookies` 提供常用的 RequestsCookieJar 风格 `set/get/get_dict/update/clear`
  接口，并支持 domain、path、Secure 和 expires；单次请求也支持 `cookies=`，
  显式 `Cookie` 请求头优先；
- `Session.headers` 与 `Session.proxies` 是可修改字典；
- Cookie、重定向、连接池与 TLS 会话由原生 Session 管理；
- `verify`、代理类型和 HTTP/3 有更严格的安全约束；
- 请求头最终顺序由 profile 决定，而不是简单照搬 Python 字典顺序。

完整中文手册见 [中文使用指南](docs/33-usage-zh-CN.md)。

## 2.1.0 发布内容

- 提供 `chrome_android_149`、`chrome_android_150`、`okhttp_4.12`、`okhttp_5.4`
  四个固定版本 profile；
- 提供 requests 风格的同步/异步 Session、CookieJar、单次 `cookies=`、代理配置和
  shared/isolated 并发池；
- Chrome profile 支持 HTTP/1.1、HTTP/2 与 HTTP/3，OkHttp profile 支持
  HTTP/1.1 与 HTTP/2；
- 请求头合并与最终 wire 顺序由底层 profile 规则处理；
- 发布 Windows x86-64 与 manylinux 2.28 x86-64 两个 CPython 3.9+ abi3 wheel，
  不上传 macOS wheel 或源码包。

2.1.0 的协议核心继承已完成真实 Pixel 4 / Android 11 数据包与会话验收的 0.1.2 候选；
本次版本变更不修改 `src`、vendored BoringSSL 或 profile 实现。CI 会逐平台构建并直接测试
最终上传的同一份 wheel，同时检查协议实现相对已验收基线没有漂移。

## 0.1.2 稳定性改进（2.1.0 已包含）

- 接受服务端合法的 HTTP/2 `SETTINGS_ENABLE_PUSH=0`，继续拒绝非法值 `1`；
- TLS 对端未发送 `close_notify` 时，由 HTTP framing 状态机判断完整性：完整响应可返回，
  不完整的 Content-Length、chunked 或 HTTP/2 响应仍会失败，连接不会复用；
- 空闲 H1/H2 连接默认最多 32 条、90 秒过期；TLS 与 H3 ticket 默认最多 64 个、
  10 分钟过期；H3 Alt-Svc 路由最多 64 条并按 LRU 淘汰；
- 阻塞式系统 DNS 解析使用进程级有界 worker/队列，超时或取消不会无限创建后台线程；
- `Session.close()` 会同时释放空闲连接和缓存的会话票据。

这些是资源边界与协议互操作修复，不改变四个 profile 的 ClientHello、HTTP/2 客户端
SETTINGS、帧顺序或请求头 wire 顺序。Flutter/Dart profile 归入 OkHttp 5.x 之后的规划；
Flutter 默认 Dart IO、Android Cronet 与 iOS URLSession 会分别建模。

## 开发与验证

```bash
python -m pip install -e ".[dev]"
python -m pytest -m "not network" -q
```

架构和证据说明位于 [`docs/`](docs/)；发布工作流见
[`docs/32-github-ci-and-publishing.md`](docs/32-github-ci-and-publishing.md)，本机四库性能
基准见 [`docs/34-performance.md`](docs/34-performance.md)。

## License

MIT

