Metadata-Version: 2.4
Name: nb-curl
Version: 0.2.0
Summary: 自带浏览器 TLS/HTTP2 指纹的 HTTP 客户端（libcurl-impersonate + BoringSSL）
License: MIT
Keywords: curl,impersonate,ja3,ja4,akamai,tls-fingerprint,http2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Classifier: Operating System :: Microsoft :: Windows
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: cffi>=1.15; implementation_name == "cpython"
Provides-Extra: certifi
Requires-Dist: certifi>=2023.7.22; extra == "certifi"
Provides-Extra: build
Requires-Dist: setuptools>=64; extra == "build"
Requires-Dist: wheel; extra == "build"
Requires-Dist: cffi>=1.15; extra == "build"
Provides-Extra: test

# nb-curl

> 这是**库本身**的说明（API 用法）。
> 想了解整个项目怎么构建 / 怎么排错 / 怎么升级，请看仓库根的
> **`../README.md`（入口文档）** 与 `../docs/`。

自带**浏览器 TLS / HTTP2 指纹**的 HTTP 客户端。底层是定制版
libcurl-impersonate（curl 8.22.0 + BoringSSL + nghttp2），上层是 requests 风格的
Python API。

```python
# 写法一：requests 风格（推荐）
from nb_curl import requests
r = requests.get("https://example.com", impersonate="chrome152")
print(r.status_code, r.http_version)   # 200 2

# 写法二：顶层 API（完全等价）
import nb_curl
r = nb_curl.get("https://example.com", impersonate="chrome152")
```

---

## 特性

| 能力 | 说明 |
| --- | --- |
| **内置指纹** | libcurl 内核里的 40 个预设（chrome99/116/131/146/150/152、firefox、safari 等），C 层实现，零 Python 开销 |
| **运行时自定义指纹** | `ja3` / `akamai` / `diy_fp` / `fp_file`（快照）在 Python 侧解析下发，**改指纹不用重编译** |
| **requests 风格 API** | `Session` / `Response` / `Headers` / `Cookies`，`get/post/put/patch/delete/head/options`；入口 `from nb_curl import requests` |
| **连接池按指纹隔离** | 切指纹会自动换连接，避免复用到旧指纹的 TLS 连接（最常见的坑） |
| **H2 复用自愈** | 服务端悄悄关连接导致 `HTTP2 framing` 错误时，自动新建连接重试并记住该 origin |
| **二进制安全** | 响应体按真实长度返回，含 `\0` 的内容不会被截断 |
| **双 FFI 后端** | CFFI API 模式（首选）与 ctypes（兜底），对外接口完全一致 |

---

## 安装 / 构建

本库需要一个预先编好的 native 动态库，以及可选的 CFFI 扩展。

```bash
# 1) 编译 native 桥接层（静态链接 libcurl-impersonate / BoringSSL / nghttp2）
bash build/windows/build_native.sh

# 2) 编译 CFFI API 模式扩展（可选，但推荐；需要本机 MSVC）
bash build/windows/build_cffi.sh

# 或者一步到位
bash build/windows/build_all.sh
```

构建依赖：
- VS2022 MSVC 14.44 + Windows SDK 10.0.26100
- 已编好的 `libcurl-impersonate`、BoringSSL、vcpkg 静态库
  （依赖已在 `deps/windows-prebuilt/`，不需要外部 vcpkg）

Python 侧只有 `certifi` 是必需依赖；`cffi` 装了才会启用 CFFI 后端。

---

## pip 安装

> **分发策略：只发二进制。** wheel 里只有编译好的 `.dll/.so/.pyd`，
> 没有任何 C 源码，别人装上就能用、但拿不到实现。
> 构建/分发的完整说明见 **[../docs/02-构建与发布.md](../docs/02-构建与发布.md)**，
> 发布到 PyPI（别人 `pip install nb-curl`）见
> **[../docs/09-PyPI发布流程.md](../docs/09-PyPI发布流程.md)**。

```bash
# 出包方（你）：先编好二进制，再出包（默认出"去注释"的发布版，自动校验无源码）
bash build/windows/build_all.sh
bash build/windows/build_wheel.sh      # 产物 dist/nb_curl-0.2.0-py3-none-win_amd64.whl

# 使用方：只需要这一个 .whl，不需要编译器、不需要源码、不需要联网编译
pip install nb_curl-0.2.0-py3-none-win_amd64.whl

# 或者从 PyPI 装（发布之后）
pip install nb-curl
```

你自己想从源码目录直接装也行：

```bash
pip install /path/to/nb_curl
```

wheel 标签是 `py3-none-<平台>`：**一个 wheel 覆盖整个 Python 3.x**。

| 情况 | 行为 |
| --- | --- |
| Python 版本与 wheel 里的 CFFI 扩展匹配 | 走 **cffi** 后端（更快、编译期 ABI 校验） |
| Python 版本不匹配 / 没装 cffi | **自动回退 ctypes 后端，功能完全一致** |

原因：只有 CFFI 扩展绑定 CPython 版本，ctypes 后端是纯 Python、
native 动态库是纯 C，三者可以独立降级。实测矩阵：

| Python | 装了 cffi | 实际后端 | 结果 |
| --- | --- | --- | --- |
| 3.11.9 | 是 | cffi | 200 ✓ |
| 3.11.9 | 否 | ctypes | 200 ✓ |
| 3.13.14 | 否 | ctypes | 200 ✓ |

```bash
# 零第三方依赖安装（走 ctypes）
pip install --no-deps dist/*.whl
```

---

## 其它操作系统

要重编的只有 native 动态库（Python 代码不用改）：

| 平台 | 产物 | 脚本 |
| --- | --- | --- |
| Windows x64 | `nb_curl_native.dll` | `build/windows/*.sh` |
| Linux x86_64 | `libnb_curl_native.so` | **`build/linux/scripts/*.sh`** |

Linux 套件（`D:\curlx\build\linux\`）已实测：Ubuntu 24.04 + Python 3.12 上
ja4/akamai 指纹与 Windows 版**逐字节一致**：

```
ja4 = t13d1516h2_8daaf6152771_e5627efa2ab1     (Windows 与 Linux 相同)
```

里面包含打补丁的 BoringSSL/curl 源码包、逐步构建脚本、CFFI 扩展构建、wheel 打包、
Dockerfile 与一键部署脚本，并记录了 Linux 侧特有的坑（BoringSSL 的 C++ 依赖需要
`-lstdc++`、curl 的 ECH 符号检测误判等）。

---

## 快速上手

```python
import nb_curl

# 一次性请求
r = nb_curl.get("https://httpbin.org/get", impersonate="chrome131", timeout=15)
r.status_code       # 200
r.json()            # dict
r.headers["server"]
r.http_version      # "2"
r.primary_ip

# 会话（保持 cookie、复用连接）
with nb_curl.Session(impersonate="chrome146") as s:
    s.get("https://example.com/login")
    s.post("https://example.com/api", json={"user": "a"})
    s.get("https://example.com/profile")

# 指定 http 版本 / 代理 / 不校验证书 / 不跟随跳转
nb_curl.get(url, http_version=11, proxies={"https": "http://127.0.0.1:7890"},
          verify=False, allow_redirects=False)

# 异步
import asyncio
async def main():
    async with nb_curl.AsyncSession(impersonate="chrome116", max_clients=16) as s:
        rs = await asyncio.gather(*[s.get(url) for url in urls])
asyncio.run(main())
```

---

## 自定义指纹

改指纹不需要重新编译，Python 侧直接下发：

```python
# 1) 用内置预设
nb_curl.get(url, impersonate="chrome116")

# 2) 用 JA3（会自动过滤 GREASE，并映射成套件名/曲线名）
nb_curl.get(url, ja3="771,4865-4866-...,0-11-10-35-16-5-51-43-13-45-65281,29-23-24,0")

# 3) 用 Akamai HTTP/2 指纹：SETTINGS|WINDOW_UPDATE|PRIORITY|PSEUDO_HEADER_ORDER
nb_curl.get(url, akamai="1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p")

# 4) 用 diy_fp 精细覆盖（叠加在内置预设之上）
nb_curl.get(url, impersonate="chrome116", diy_fp={
    "tls_grease": True,
    "http2_no_priority": True,
    "tls_permute_extensions": False,
    "header_order": "...",
})

# 5) 组合：预设打底 + ja3/akamai 覆盖
nb_curl.get(url, impersonate="chrome131", ja3=..., akamai=...)
```

`diy_fp` 可用键见 `nb_curl.diy_fp_fields()`，拼错会直接报错，
不会静默忽略。

内置指纹的可用目标：

```python
nb_curl.available_targets()      # 40 个
nb_curl.is_available("chrome150")  # True
nb_curl.RECOMMENDED_TARGETS
```

---

## 关于 FFI 后端：为什么默认用 CFFI API 模式

实测（`bench_ffi.py`，200k 次调用取最优）：

| 后端 | 单次 int 调用 | 单次 char* 调用 |
| --- | --- | --- |
| ctypes | 118 ns | 494 ns |
| CFFI ABI 模式 | 116 ns | 437 ns |
| **CFFI API 模式** | **101 ns** | **412 ns** |

**结论：CFFI API 模式比 ctypes 快约 1.2x。但这对端到端时延几乎没有意义**——
一次 HTTPS 请求约 50–300 ms，每请求约 40 次跨界调用，即便按 ctypes 的峰值估算，
FFI 也只占 **不到 0.1%**。真正的耗时全在网络与 TLS。

那为什么还是默认用 CFFI API 模式？理由是**类型安全**而非速度：

- CFFI API 模式在编译期把 `cdef` 与真实头文件（`nb_curl_bridge.h`）一起编译，
  **结构体字段错位/签名不匹配会在编译期直接报错**，不会变成运行期的静默内存错误；
- 不需要手写 `argtypes`/`restype`，代码更干净。

选择规则（环境变量 `NB_CURL_BACKEND`）：

```bash
NB_CURL_BACKEND=auto    # 默认：优先 cffi，缺失/加载失败自动回退 ctypes
NB_CURL_BACKEND=cffi    # 强制 cffi，失败直接报错（CI 用，能暴露构建问题）
NB_CURL_BACKEND=ctypes  # 强制 ctypes（对比/排查）
```

```python
nb_curl.backend_name()       # 'cffi' / 'ctypes'
nb_curl.backend_info()       # {'backend': ..., 'native_lib': ..., 'curl': ...}
nb_curl.available_backends() # ['cffi', 'ctypes']
```

两个后端实现同一个接口，且 `tests/test_nb_curl.py` 的 **第 8 节会实测两者
指纹输出完全一致**。

---

## 测试

```bash
./.venv/Scripts/python.exe tests/test_nb_curl.py
```

- **1–5 节** 打真实站点 `xcctls.top`（只有真实 TLS 才能验证指纹）；
- **6 节** 打本地回显服务 `tests/local_server.py`，精确断言"客户端到底发了什么"。
  公共 httpbin 类站点会拦截 curl-impersonate 指纹（返回 405 / 断连），
  不适合做基准，故改用本地服务；
- **7–9 节** 连接复用自愈、双后端一致性、并发。
- **10–12 节** 参数取值校验、xcctls 快照与内置 chrome152、proxy/proxies 语义。

---

## 目录结构

```
nb_curl/
  __init__.py           对外 API
  session.py            Session / 连接池 / 重试 / 请求装配
  models.py             Headers / Cookies / Request / Response
  fingerprint.py        ja3 / akamai / diy_fp 解析
  impersonate.py        内置 target 解析与探测
  _native.py            FFI 后端选择层（cffi / ctypes）
  _backend_cffi.py      CFFI API 模式后端
  _backend_ctypes.py    ctypes 后端
  _cffi_build.py        CFFI 扩展构建脚本（内部注入 MSVC 环境）
  requests.py           requests 风格入口（from nb_curl import requests）
  const.py / exceptions.py / aio.py
  _lib/nb_curl_native.dll 编好的 native 桥接层
  _nb_curl_cffi*.pyd      CFFI API 模式扩展
native/
  nb_curl_bridge.h        稳定 C ABI
  nb_curl_bridge.c        实现（内置指纹转发 + 自定义指纹下发）
build/windows/{build_native,build_cffi,build_all,build_wheel}.sh
../build/tools/bench_ffi.py            FFI 开销基准
tests/
  test_nb_curl.py         功能自测（172 项）
  local_server.py       本地回显测试服务
```

---

## 已知问题 / 设计取舍

- **`httpbin.org` 等公共回显站点会拦截 curl-impersonate 指纹**，返回 405 或断连。
  这是站点侧的 anti-bot 策略，不是本库的问题——测试因此改用本地服务。
- **某些服务端（如 `xcctls.top`）在每次响应后关闭 HTTP/2 连接**，libcurl 会尝试
  复用已死连接并报 `Error in the HTTP2 framing layer`。本库会自动新建连接重试，
  并记住该 origin 后续直接新建连接（`Session._no_reuse_origins`）。
  可用 `max_retries=0` 关闭该行为（仅对幂等方法重试）。
- `AsyncSession` 基于线程池实现，不是零开销的真异步。若需要极致并发，建议多进程
  或多个 `Session` 实例。
- 目前只在 Windows x64 + MSVC 上构建验证过。
