geektls
多语言 TLS/HTTP 全栈指纹伪造库:对 ClientHello 逐字节可控,覆盖 TLS(JA3/JA4)、HTTP/2(Akamai 指纹 + HPACK 编码策略)、HTTP/3 + QUIC、TCP 四层指纹,提供 Python / Node.js / Go 三种语言的一等 API。
当前状态:TLS 与 HTTP/2 已交付,并经外部 oracle(tls.peet.ws)实测验证;HTTP/3 的 Chrome 149 已完成真机采集(QUIC 内层 JA4 与真机逐字符一致),Firefox / Safari H3 待实测;TCP 仅 TTL/MSS 可承诺(window / window_scale / options 仅 Linux 探测模式,不影响真实连接)。各维度精确现状见 docs/capability-matrix.yml。
目录
- 功能矩阵
- 安装
- 快速上手
- 命令行(geektls CLI)
- 指纹怎么传:整体与局部
- Python API 参考
- Node.js API 参考
- Go API 参考
- 使用场景
- 预设体系
- 自校验
- 性能摘要
- 与同类项目对比
- 已知边界与不足
- 开发与发布
- 文档索引
- 非目标
功能矩阵
| 维度 | 状态 | 验证 |
|---|---|---|
| TLS ClientHello 逐字段可控(ciphers / 扩展序 / GREASE / key_share / MLKEM / ALPS / RSL / DC / padding / ECH) | ✅ | tls.peet.ws 实测 JA3 / JA4 一致;QUIC Initial 字节级嗅探 |
| 真 ECH(config_list 注入) | ✅ | cloudflare-ech.com 实测 ECHAccepted=true |
| 指纹入口:完整 profile / JA3 / JA4 / JA4R / ClientHello-hex,附 JA3/JA4 回读 | ✅ | FoxIO 官方向量 + 线上 round-trip;JA4 短哈希反查内置预设 |
| HTTP/2 帧层(SETTINGS 序 / WINDOW_UPDATE 三态 / priority / 伪头序 / 首流号) | ✅ | tls.peet.ws Akamai 指纹一致;另有原始帧级断言 |
| HTTP/3 + QUIC(SETTINGS / 伪头序 / GREASE 帧 / 内层 ClientHello 同 profile) | ✅ | Chrome 149 真机采集:QUIC 内层 JA4 与真机一致;transport params 可控。Initial 布局不可控、Firefox/Safari H3 待实测 |
| HTTP/2 HPACK 编码策略(chrome / firefox / safari / generic 四档) | ✅ 255/394 预设覆盖 | 逐字节块表示断言;chrome / firefox 档有源码或字节级证据,safari 档为保守近似 |
| H2/H3 racing + Alt-Svc | ✅ H3 默认关(h3=True / protocols 显式开) |
协议选择用例 + 线上 ALPN 断言 |
| 请求头顺序可控(preserve / input / random) | ✅(默认 preserve) | 原始抓包逐行核对线上头序(集合不变、Host 恒最前) |
| 身份自洽(自带 UA ↔ 客户端提示) | ✅(identity_sync="auto") |
冲突时 sec-ch-ua* 校正到调用方 UA 并告警;TLS/H2 不伪造 |
| 连接池(H2 多路复用 / H1 keep-alive / H3 共享 transport,并发安全) | ✅ | go test -race 零竞争;echo 侧连接数断言 |
| 流式上传(H1 chunked / H2 DATA,三语言绑定迭代器入参) | ✅ | 线上字节逐字节断言 |
| 响应自动解压(gzip / deflate / br / zstd,多编码链流式生效) | ✅ | 六路径矩阵 + 跨压缩块边界流式断言 |
| WebSocket(wss / ws,RFC 6455 + permessage-deflate,握手走指纹链路) | ✅ | RFC 服务端断言头序 / 帧 / 关闭码;独立 flate 对端断言压缩流 |
明文 http:// / ws:// |
✅(H1;无 TLS,selfcheck 恒为零值) | 引擎用例 + pytest 一组;与 force_http3 互斥(明确报错) |
命令行 geektls(version / presets / describe / check-profile / request) |
✅ | 子命令冒烟 + 退出码口径(0/1/2) |
| Python asyncio(AsyncSession,线程池异步) | ✅ | asyncio 并发 20 请求用例 |
| TCP(TTL/MSS setsockopt,按平台分文件) | ✅(Windows 无 MSS) | getsockopt 读回;CI 跨平台编译门禁 |
| Python 请求语义对齐(cookie jar / timeout 元组 / auth / 重定向开关 / 表单编码) | ✅ | pytest 10 条(本地明文服务端,零外部依赖) |
低层装载自动兜底(hex/ja3/ja4r:HTTP 身份 + 扩展载荷,identity= / profile_patch= 可控) |
✅ | 三入口回环 selfcheck(本地 HTTPS)+真机 pcap 闭环(缺身份头 403 → 补齐后 404) |
| Python 上传与流式扩展(multipart/form-data、SSE text/event-stream) | ✅ | httpbin 原字节断言;本地 SSE 服务端全形态用例 + 真实 SSE 流实测 |
| Python / Node / Go 绑定 | ✅ | pytest 34 通过(2 跳过)、node:test 18、go test 通过;跨语言 JA4 三方一致 |
安装
pip install geektls # Python(manylinux_2_28 x86_64/aarch64、macOS 11.0+ arm64/x86_64、Windows x64,动态库随 wheel 分发)
npm install geektls # Node.js(Windows x64 / Linux x64 / macOS Apple Silicon;⚠️ Intel Mac 暂不支持)
go get github.com/geekbyter/geektls/core@v0.2.0 # Go(monorepo 子目录模块;pkg.go.dev/github.com/geekbyter/geektls/core)
已发布到 PyPI 的 wheel(0.1.5 起)覆盖五平台:manylinux_2_28 x86_64 / aarch64、macosx_11_0 arm64 / x86_64、win_amd64 (构建矩阵见 .github/workflows/release-pypi.yml;其余平台安装会提示无匹配版本; 不提供 sdist——从源码构建需要 Go + C 工具链,本库只发 wheel)。
⚠️ 老版本的平台覆盖(实测 PyPI 索引):
| 版本 | 已上传的平台 | 说明 |
|---|---|---|
| 0.1.0 | 仅 manylinux_2_34_x86_64 |
没有 Windows;Linux 基线是 glibc 2.34 |
| 0.1.1 – 0.1.3 | manylinux_2_34_x86_64 + win_amd64 |
同上 |
| ≥ 0.1.4 | 五平台:manylinux_2_28 x86_64/aarch64、macosx_11_0 arm64/x86_64、win_amd64 |
Linux 基线 2.34 → 2.28(兼容面更大);macOS 与 Linux aarch64 从这里才开始提供 |
⇒ macOS、Linux aarch64、或 glibc < 2.34 的系统上 pin 到 0.1.0–0.1.3 会直接
No matching distribution found;本库不发 sdist,没有源码安装这条退路。
Python 一行自检:
python -c "import geektls,json;print(json.dumps(geektls.version()));print(len(geektls.list_presets()),'presets')"
# {"abi": 1, "core": "0.2.0", "utls": "refraction-networking/utls v1.8.2; bogdanfinn/utls v1.7.8-barnius; ..."} / 394 presets
version()["utls"]是指纹栈溯源:报告动态库里实际链接的 uTLS / fhttp / quic-go-utls 版本(由二进制 build info 推导,跟随go.mod自动更新,不会漂移),vendor fork 会显示成bogdanfinn/fhttp v0.6.9 => ./third_party/fhttp。出问题时把这一串贴进 issue 即可定位栈版本; 极端裁剪的构建可能为空串,调用方需容忍。
快速上手
Python(pip install geektls,动态库随包):
from geektls import Session
with Session(impersonate="chrome_150") as s:
r = s.get("https://example.com")
print(r.status_code, r.ok, r.used_protocol) # 200 True h2
print(r.headers["content-type"]) # 大小写不敏感
print(r.text[:200]) # 直接当文本用
print(r.selfcheck["ja4"]) # 本次握手的 JA4 自算
for line in r.iter_lines(): # 需要流式时才用迭代
...
Node.js(npm install geektls):
const { Session } = require('geektls');
const s = new Session({ impersonate: 'chrome_150', timeout: 20 });
const r = await s.get('https://example.com', { params: { q: 1 } });
console.log(r.statusCode, r.ok, r.reason); // 200 true OK
console.log(r.header('content-type')); // 大小写不敏感
console.log(await r.text()); // 直接当文本用
console.log((await r.json()).slideshow); // 直接当 JSON 用
for await (const chunk of r.iterContent()) { ... } // 需要流式时才迭代
s.close();
Go(import gtls "github.com/geekbyter/geektls/bindings/golang";包名 geektls):
// 引擎直连(要用到响应便捷方法时走这条;只要 net/http 习惯可用上面的 RoundTripper 形态)
p, _ := profiles.Get("chrome_150")
sess, _ := engine.NewSession(p, engine.SessionOptions{})
defer sess.Close()
resp, _ := sess.Do(&engine.Request{Method: "GET", URL: "https://example.com"})
defer resp.Close()
fmt.Println(resp.StatusCode(), resp.OK(), resp.Reason()) // 200 true OK
fmt.Println(resp.Header("content-type")) // 大小写不敏感
text, _ := resp.Text() // charset → utf-8 → gb18030 → latin-1
var v map[string]any
_ = resp.JSON(&v)
命令行(geektls CLI)
不想先写代码就想看效果时:
make cli # 构建到 build/geektls(Windows 下 build/geektls.exe)
build/geektls version
build/geektls presets --grade self # 只看自测集;--json 结构化;--name 过滤
build/geektls describe chrome_154_windows | jq .tls.detail.extensions
build/geektls check-profile chrome_133 # 离线自检:预设名 / ja3: / ja4: / ja4r: / hex
build/geektls request https://example.com -i --selfcheck
build/geektls request http://127.0.0.1:8000/ --ja4 t13d1516h2_8daaf6152771_02713d6af862
- 指纹构造与绑定同一批入口(
profiles.Get/FromJA3/FromJA4R/FromClientHelloHex/ResolveJA4Profile),用户自带输入同样过一遍NormalizeForReplay自洽归一 —— CLI 看到的就是代码里跑的; --selfcheck把"本次握手实际发出的 JA3/JA4"打到 stderr ⇒ stdout 只放正文,可直接管道或重定向;- 退出码:
0= 传输成功(含 4xx/5xx,curl 口径;加--fail让 4xx/5xx 返回 1)、1= 配置或传输错误(原因在 stderr)、2= 用法错误。
从抓包复刻指纹(import_pcap / import-pcap)
Wireshark/tcpdump 的抓包直接变成可复用指纹——不需要 tshark、不需要解密。 三种语言内置同一解析核,两步直通(以 Python 为例,Node/Go 同构):
import geektls
res = geektls.import_pcap("chrome.pcapng", ua=UA) # ① 文件进(UA 说明见下)
rec = res["records"][0] # 记录出:ja3/ja4/sni/tcp 形态
with geektls.Session(clienthello_hex=rec["clienthello_hex"]) as s: # ② 直通装载
r = s.get("https://tls.peet.ws/api/all")
print(r.selfcheck) # 线上对拍:实际 JA3/JA4 vs 期望
// Node.js
const res = geektls.importPcap({ path: "chrome.pcapng", ua: UA });
const client = new geektls.Session({ clienthello_hex: res.records[0].clienthello_hex });
// Go
res, _ := gtls.ImportPcap("chrome.pcapng", >ls.PcapOptions{UA: UA})
s, _, _ := gtls.NewSessionFromClientHelloHex(res.Records[0].ClientHelloHex, nil)
TLS 面怎么处理:clienthello_hex 是抓包中 ClientHello 的 record 层原始字节
(16 03 01 开头),装载后 TLS 面逐字节复现(cipher 序、扩展序与载荷、ALPN、
ECH 外层等);client_random / key_share / GREASE 等随机项每次自动重生成。
三层验证:记录自带 ja3/ja4(离线)→ check_profile 细看解析(离线)→ 请求后
selfcheck 与记录值对拍(线上)。
HTTP 身份(identity):clienthello_hex / ja3 / ja4r 这类低层装载不带
浏览器请求头套件——引擎会自动借用"最相似内置预设"的 identity 节(按 ciphers
集合 / 扩展集合 / ALPN 匹配,相似度 ≥ 0.75 才借),并在响应 warnings 里说明借了谁
(identity_auto)。不补身份头的请求会以 Go 默认 UA 上线,容易被云 WAF 的"精准访问
控制"直接拦截(实测同一 ClientHello 逐字节复刻:缺身份头 403、补齐后 404)。要指定
或关闭:identity="预设名" / identity={...}(头表)/ identity=False——三绑定同名
参数(Node identity、Go Options.Identity)。
载荷兜底:ja3 / ja4r 的扩展载荷按"最相似内置预设"的真实形态替换 / 补全
(payload_borrowed 告警说明来源;入口自带 groups / points(ja3)与 sig_algs(ja4r)
保持原样)。只想改个别字段(如运行时剔除某个扩展):profile_patch(JSON 片段深合并、
数组整体替换,先于兜底生效;三绑定同名参数)。
常规用法:all_streams / all 导出全部流(记录里的 sni 区分同名或异名
流)、stream=N 挑第 N 条、tcp_only / tcpOnly 只取 TCP 形态、data 字节入口。
被拒的流在 skipped 中逐条给出原因(resumption、缺 SYN、抓包缺口、无 CH),
records 为空不是异常。未补 UA 时记录带 missing_ua 警告(不影响 TLS 面装载;
HTTP 头在 TLS 隧道内,明文抓包看不到 UA,需要从浏览器侧抄)。
CLI(生成可入库预设时才需要,与绑定同一解析核):
build/geektls import-pcap --pcap chrome.pcapng --ua "$UA" -o fp.json
tests/e2e/cmd/gen-profiles -record fp.json -out ./presets
完整示例见各绑定 README(Python /
Node.js / Go);
支持 pcapng 与 pcap classic(含 tcpdump -i any 的 cooked 链路);
设计与边界见 docs/12-pcap-import.md。
协议选择 / 请求头顺序 / 身份自洽
三个开关,默认行为与旧版完全一致,需要时才开:
| 关心的事 | 参数 | 默认 | 可选值 |
|---|---|---|---|
| 走哪个 HTTP 版本 | protocols / h3 |
["h1.1","h2"](不含 H3) |
["h1.1"] 只 H1.1 | ["h2"] 只 H2 | ["h2","h3"] 允许 H3 | ["h3"] 强制 H3;便捷写法 h3=True |
| 请求头顺序 | header_order |
preserve(按预设声明序) |
input(按你给的顺序)| random(打乱) |
| 自带 UA 与预设不一致 | identity_sync |
auto(校正客户端提示 + 告警) |
off(不校正) |
为什么 H3 默认关:同一个 URL 走 H2 还是 H3 是可观测差异(协议入口、QUIC 侧指纹都不同),
不该由"预设里带 http3 节"替用户决定;curl_cffi / requests 生态的默认也是 H2,H3 要显式开。
以前只要预设带 http3.enabled + h2_race_ms 就自动竞速 H3,现在必须 h3=True 或
protocols 含 "h3";请求级 force_http3=True 语义不变(失败不回落)。
协议选择:Python
from geektls import Session
# 1) 默认(什么都不给):h1.1 + h2,**不碰 H3** —— 与 curl_cffi / requests 同口径
with Session(impersonate="chrome_154_windows") as s:
r = s.get("https://example.com/")
print(r.used_protocol, r.selfcheck["negotiated"]["alpn"]) # 例:h2 h2
# 2) 只要 H1.1:ALPN 里不再 offer h2(对端偏好 h2 也只能给 http/1.1)
with Session(impersonate="chrome_154_windows", protocols=["h1.1"]) as s:
print(s.get("https://example.com/").used_protocol) # http/1.1
# 3) 只要 H2:对端不支持 h2 时**直接失败**,不静默回落 H1
with Session(impersonate="chrome_154_windows", protocols=["h2"]) as s:
print(s.get("https://example.com/").used_protocol) # h2
# 4) 允许 H3(竞速/Alt-Svc 优先,失败落回 TCP);h3=True 是便捷写法
with Session(impersonate="chrome_154_windows", h3=True) as s:
print(s.get("https://example.com/").used_protocol) # h3 或 h2(看谁先拿到响应头)
# 5) 只走 H3(会话级强制,失败即报错,不回落)
with Session(impersonate="chrome_154_windows", protocols=["h3"]) as s:
print(s.get("https://example.com/").used_protocol) # h3
# 6) 请求级强制(0.1.6 起语义不变:仍是"必须 H3",但会话 protocols 显式排除 h3 时冲突报错)
with Session(impersonate="chrome_154_windows") as s:
r = s.request("GET", "https://example.com/", force_http3=True)
要点:h3=True 与 protocols=[...] 不能同时给(两种意图打架,直接报错,不静默取其一);
h3=False 与 protocols 同给不报错(常见写法,无额外意图)。开了 h3 但预设没有 http3 声明
会在建会话时报错,而不是悄悄退回默认 QUIC 参数。
协议选择:Node
const { Session } = require('geektls');
const s = new Session({ impersonate: 'chrome_154_windows' }); // 默认 h1.1+h2
const s1 = new Session({ impersonate: 'chrome_154_windows', protocols: ['h1.1'] }); // 只 H1.1
const s2 = new Session({ impersonate: 'chrome_154_windows', h3: true }); // 允许 H3
const s3 = new Session({ impersonate: 'chrome_154_windows', protocols: ['h3'] }); // 只 H3
const r = await s.get('https://example.com/');
console.log(r.usedProtocol, r.selfcheck?.negotiated?.alpn);
协议选择:CLI
build/geektls request https://example.com --protocols h1.1 --selfcheck # 只 H1.1(ALPN 打 stderr)
build/geektls request https://example.com --protocols h2 # 只 H2
build/geektls request https://example.com --h3 # 允许 H3
build/geektls request https://example.com --protocols h3 # 只 H3
build/geektls request https://example.com --http3 # 请求级强制
请求头顺序:三档
with Session(impersonate="chrome_154_windows") as s:
# 默认 preserve:按 profile.http1.header_order 归位(= 与 0.1.6 之前逐字节相同)
s.get(url)
with Session(impersonate="chrome_154_windows", header_order="input") as s:
# 按**你传入字典的顺序**发(Host 仍在最前);requests 用户自己排好序时用这个
s.get(url, headers={"accept-language": "en-US,en;q=0.9", "accept": "*/*"})
with Session(impersonate="chrome_154_windows", header_order="random") as s:
# 每次请求打乱普通头顺序(Host 恒最前);绕"顺序即信号"的检测用
s.get(url)
CLI 同理:--header-order preserve|input|random。
头序三档的取舍:真浏览器的头序是固定的,random 是"对抗顺序检测"的手段而不是更真——
所以默认 preserve,且它会破坏可复现性(回归/对拍时别开)。
自带 UA 时的身份自洽
WIN_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/161.0.0.0 Safari/537.36")
# 预设是 macOS 形态,但你写 Windows UA ⇒ 默认 auto:sec-ch-ua*/platform/mobile 跟着校正
with Session(impersonate="chrome_154_macos") as s:
r = s.get(url, headers={"user-agent": WIN_UA})
print(r.warnings) # ["identity_sync: … TLS/JA3/JA4/H2 仍为该预设 …"]
# 想要连 TLS 也一致:换同平台变体预设(它的 ClientHello 本来就不同),而不是靠改头
with Session(impersonate="chrome_154_windows") as s:
s.get(url, headers={"user-agent": WIN_UA}) # TLS 与 UA 都是 Windows 形态
# 不想要任何校正/告警:off(= 旧行为)
with Session(impersonate="chrome_154_macos", identity_sync="off") as s:
s.get(url, headers={"user-agent": WIN_UA})
CLI:--identity-sync auto|off。
身份自洽的边界:你自带 user-agent 时,identity_sync="auto" 会把
sec-ch-ua(版本号)/sec-ch-ua-platform/sec-ch-ua-mobile 校正到该 UA,因为这几项与 UA
同源、不一致是明显破绽。但 TLS/JA3/JA4/H2 不会跟着变——凭空改出来的指纹不属于任何真实
浏览器,比不一致更糟。想要"UA 与 TLS 也一致",正确做法是换同平台变体预设
(chrome_154 的 _windows / _macos / _android 变体的 TLS 真的不同),库会把这句写进
r.warnings。查变体:geektls.list_presets() 或 geektls check-profile <名>。
指纹怎么传:整体与局部
Session(...) 的指纹入参六选一(互斥,同时给以靠前的为准,优先级按下列顺序):
| 入参 | 语义 | 保真度 | 备注 |
|---|---|---|---|
impersonate="chrome_150_windows" |
用内置预设(394 条) | 高(多数有 E1/E2 证据) | 名字是全名:chrome_154_macos、chrome_154_windows、okhttp_3_12_12…(没有裸 chrome_154) |
profile={...} |
自带完整指纹(profile JSON schema) | 完全按你给的来 | 也接受 JSON 文本(profile='{"name":...}'),免去 json.loads |
ja3="771,4865-...-23,4588-29-23-24,0" |
只给 JA3 | 有损:扩展只有 type,负载全缺 | 载荷与 HTTP 身份按"最相似内置预设"自动补齐(payload_borrowed / identity_auto 告警),check_profile 会逐条列出 warnings |
ja4r="t13d1516h2_002f,..._..._..." |
只给 JA4R(含 cipher/扩展/sig_algs 列表) | 中:列表有序化丢失(extensions_sorted 告警) |
想"逐字节复刻"用这个或完整 profile;载荷按最相似预设补齐;带 ECH(65037) 时按 GREASE 近似并告警(ech_assumed_grease),因为 raw 串不含负载 |
ja4="t13d1516h2_8daaf6152771_d8a2da3f94cd" |
只给 JA4 短哈希 | 哈希不可逆 | 引擎在内置预设里反查同一 JA4 并采用其参数(ja4_resolved_to_preset 告警);找不到会明确报错,不会编近似指纹 |
clienthello_hex="160301..." |
直接给原始 ClientHello 字节 | 最高(等于抓包重放的结构) | 与生成器/采集端同一条解析路径 |
没传的部分如何保持自洽(这是"局部指纹"能用的关键):
- 重放自洽归一(
core/profiles/replay.go):带pre_shared_key(41)的双向约束——声明 TLS 1.3 就必须有 41 空占位且位于扩展末尾(RFC 8446),不声明就必须没有(否则 ClientHello 结构非法)。补的是空占位:无票据时线上省略、也不计入 JA3/JA4,所以首次握手的指纹不变;不补的话,一旦会话复用命中票据,uTLS 会直接 panic。 - 损失告警:JA3 入口会报
grease_lost/extension_payloads_lost;JA4R 会报extensions_sorted;JA4 反查会报ja4_resolved_to_preset。有损入口会明确告知损失,而不是假装保真。 - HTTP 身份与载荷兜底:
ja3/ja4r/clienthello_hex装载时自动借用"最相似内置预设"的 identity 节与扩展载荷(告警identity_auto/payload_borrowed说明借了谁);identity=可显式指定(预设名 / 头表)或False关闭,profile_patch=做增量覆盖。 - 离线验证:
geektls.check_profile(...)不发包,直接告诉你"这么传会发出什么"(JA3/JA4/wire 长度/warnings):
import geektls
# 整体:完整 profile 文本
print(geektls.check_profile('{"name":"mine","tls":{"detail":{"ciphers":["grease","0x1301"],"extensions":[{"type":43,"versions":["grease","0x0304","0x0303"]}]}}}')["ja4"])
# 局部:只给 JA3(会看到补占位 + 负载丢失的告警)
print(geektls.check_profile("771,4865-4866-4867-49195-49199,43-10-11-13-16-23-5-0-65281,4588-29-23-24,0"))
# 只给 JA4 短哈希(反查内置预设)
print(geektls.check_profile("t13d1516h2_8daaf6152771_d8a2da3f94cd"))
Python API 参考
模块级函数
| 函数 | 签名 | 说明 |
|---|---|---|
version() |
-> dict |
{"abi":1,"core":"0.2.0","utls":"<指纹栈版本串>"};启动时可用它断言 ABI 匹配,utls 用于溯源(见上文) |
init(options=None) |
-> None |
幂等初始化钩子(当前无全局状态,留作后续) |
last_error() |
-> dict |
最近一次失败的结构化错误(code/message/op) |
list_presets() |
-> list[str] |
全部内置预设名(排序,394 条;返回规范名,旧名是别名) |
describe_preset(name) |
-> dict |
预设展开后的规范 JSON(ciphers/扩展/H2/H3/身份头全展开) |
check_profile(spec) |
-> dict |
离线自检:profile JSON / JA3 / JA4 / JA4R / hex → {ja3,ja3_hash,ja4,wire_len,warnings} |
GeekTLSError |
异常类 | 所有失败都是它;.code 结构化错误码(invalid_config/request_failed/…) |
default_session(**options) |
-> Session |
模块级共享会话;首次调用可传 Session(...) 参数配置(之后只读,再传报错) |
get/head/post/put/patch/delete/options(url, **kwargs)、request(method, url, **kwargs) |
-> Response |
requests 风格快捷 API,走 default_session()(连接池与 Cookie jar 随之共享;与 requests 的模块级 API 同语义) |
Session
Session(impersonate=None, *, profile=None, ja3=None, ja4=None, ja4r=None,
clienthello_hex=None, identity=None, profile_patch=None, headers=None,
proxies=None, timeout=None,
read_timeout=None, resolve=None, local_address=None, ip_version=None,
verify=None, cert=None, cert_key=None, allow_redirects=None,
max_redirects=None, cookies=None, auth=None, trust_env=None, **options)
| 参数 | 类型 | 说明 |
|---|---|---|
impersonate / profile / ja3 / ja4 / ja4r / clienthello_hex |
str / dict / str | 六选一(见上一节) |
identity |
str / dict / False |
低层装载(ja3/ja4r/clienthello_hex/不带 identity 的 profile)时的 HTTP 身份:预设名(借其 identity 节)/ 头表(如 {"User-Agent": …})/ False 关闭自动兜底。不传时自动借用"最相似内置预设"(见"指纹怎么传"节),并在响应 warnings 说明 |
profile_patch |
dict / str(JSON) | 以最终 profile 为底的增量覆盖(深合并:对象递归、数组整体替换,与 profile 的 extends 同规则)——从预设/抓包出发只改要改的字段(增删扩展、换 ALPN、覆盖身份头…),免去手抄全量 profile;先于身份/载荷兜底生效 |
headers |
dict / list[tuple] | 会话默认请求头(按给定顺序发出) |
proxies |
dict / str | requests 风格;引擎一次只用一个代理 URL({"https": ...} / {"all": ...})。scheme:http/https(CONNECT 隧道)、socks5(本地先解析域名)、socks5h(域名交给代理解析)、socks4/socks4a(仅 IPv4 目标;只有 user id 无口令位,URL 给口令即报错) |
trust_env |
bool | requests 同名,映射到引擎 proxy_from_env:False ⇒ 不读 HTTPS_PROXY/ALL_PROXY,只认显式 proxies;默认 True 会读,并按 NO_PROXY 与回环目标(localhost/127.0.0.1/::1)豁免 |
timeout |
float 秒 / (connect, read) |
单值映射到引擎 timeout_ms(覆盖"dial+TLS+响应头");元组时 connect 进 timeout_ms、read 进 read_timeout_ms(任一项可为 None),请求级可用 timeout_ms= 覆盖 |
read_timeout |
float 秒 | 映射到引擎 read_timeout_ms:每次读 body 的空闲上限(默认 0=不限,与旧行为一致);请求级可用 read_timeout_ms= 覆盖。超时是结构化错误 code="read_timeout",调用线程毫秒级返回,底层连接作废 |
resolve |
dict[str, str] | curl --resolve:{"host": "1.2.3.4"} 或 {"host:8443": "..."}(键精确匹配,不做通配/子域;值必须是 IP 字面量)。只改"连到哪":SNI / Host / 伪头 / ClientHello 仍用 URL 里的原域名,指纹字节不变。只在引擎本地解析的那几档生效(直连 / netstack / socks5 / socks4);socks5h/socks4a/CONNECT 由代理解析,钉位不参与(不静默改道) |
local_address |
str | curl --interface 的 IP 形态 / httpx local_address:绑出网源地址,只收 IP 字面量(网卡名不支持)。隐含协议族,与 ip_version、resolve 取值冲突建会话即 ValueError;tcp.mode=netstack 下报错(用户态栈源地址固定)而不是忽略 |
ip_version |
str / int | curl -4 / -6:"4" / "6"(4、"v4"、"ipv4" 同义),限定解析与拨号族;命中的地址全被过滤时是明确的解析错误 |
verify |
bool / str / bytes / Path | True(默认,系统信任库)/ False(跳过校验)/ CA bundle 路径、含 .pem/.crt/.cer 的目录、或内联 PEM(自持信任库,替换系统根)。其他类型直接 ValueError,不会静默退回系统根 |
cert |
str / bytes / Path / tuple | mTLS 客户端证书:路径或 PEM(证书+私钥可同文件)、(证书, 私钥);三元组的 password 必须为 None(不支持加密私钥) |
cert_key |
str / bytes / Path | 单独给私钥(覆盖 cert 元组第二项) |
allow_redirects |
bool | False ⇒ 不跟随重定向(会话级;请求级也可传,请求级覆盖会话级) |
max_redirects |
int | 重定向上限(默认 10;会话级与请求级都可用) |
cookies |
dict / list[tuple] / str / Cookies / bool |
dict 等形态 ⇒ 会话级 jar(每请求渲染成 Cookie 头,响应里的 Set-Cookie 自动并入);True/False ⇒ 只开关引擎 jar |
auth |
tuple / 对象 | (user, password) 或带 username/user + password 属性的对象 ⇒ HTTP Basic;自己传了 Authorization 头时以你的为准 |
protocols |
list[str] | 允许的 HTTP 协议集合(顺序无关):"h1.1" / "h2" / "h3"。默认 ["h1.1","h2"],不含 H3(与 curl_cffi 同口径)。只要 ["h1.1"] ⇒ ALPN 收窄到 http/1.1;只要 ["h2"] ⇒ 对端不支持就失败(不静默回落);只要 ["h3"] ⇒ 会话级强制 H3 |
h3 |
bool | protocols 的便捷写法:True = 默认集合加 "h3"(预设需带 http3 声明)。h3=False 与 protocols 同给不报错,h3=True 与 protocols 同给报错 |
header_order |
str | 请求头顺序:"preserve"(默认,按 profile.http1.header_order)/ "input"(按你传入的顺序)/ "random"(打乱,Host 仍最前)。⚠️ random 与真浏览器不符且破坏可复现性 |
identity_sync |
str | "auto"(默认):你自带 user-agent 与预设身份不一致时,自动把 sec-ch-ua / sec-ch-ua-platform / sec-ch-ua-mobile 校正到该 UA,并在 r.warnings 里说明"TLS/H2 仍是该预设";"off" = 不校正 |
**options |
— | 其余按引擎会话原名透传(proxy/proxy_from_env/timeout_ms/read_timeout_ms/redirect_max/insecure_skip_verify/auto_decompress/cookie_jar/ca_bundle/client_cert/client_key/resolve/local_address/ip_version);不在名单里的键绑定层直接 ValueError |
| 方法 | 说明 |
|---|---|
request(method, url, *, params=None, data=None, json=None, headers=None, body=None, timeout=None, timeout_ms=None, read_timeout=None, read_timeout_ms=None, proxy=None, proxies=None, cookies=None, auth=None, allow_redirects=None, max_redirects=None, stream=None, force_http3=None, **kwargs) |
通用请求;头到达即返回,body 按需读取。proxies / cookies / auth / allow_redirects / max_redirects 请求级覆盖会话级 |
get / post / put / patch / delete / head / options(url, **kwargs) |
便捷动词,等价于 request |
websocket(url, headers=None, timeout=None, compress=False) |
升级 WebSocket(见下节),返回拉模型 WebSocket;compress=True 握手 offer permessage-deflate |
close() |
释放会话(挂连接池/transport) |
with Session(...) as s: |
自动 close() |
⚠️
verify/cert是会话级:传给request()会明确报错而不是静默失效(要按请求切换就新建 Session)。allow_redirects/max_redirects/proxies/cookies/auth请求级也能用。⚠️ 拼错的关键字不会静默吞掉:core 的两处 JSON 解码都不严格(client config 只查指纹入参那六个键,
SessionOptions忽略未知字段),所以名单校验做在绑定层——Session(...)收到未知会话选项、request()收到未知请求级 kwargs 都立即报错(Node 侧同理:new Session({insecure_skip_verfy: true})抛TypeError)。这类选项决定"校验开不开、超时走不走、连接钉到哪",绝不能配了却没生效。
multipart/form-data 上传
用 Multipart 构造器组装表单(boundary 为浏览器形态 ----WebKitFormBoundary…;
同名 part 可重复添加,用于"同一字段多文件"):
from geektls import Multipart
mp = Multipart()
mp.add_part("image", local_path="./image.png", content_type="image/png")
mp.add_part("image", data=b"...", filename="second.png", content_type="image/png")
mp.add_part("note", data="hello") # 无文件名的普通字段
with Session(impersonate="chrome_154_windows") as s:
r = s.post("https://httpbin.org/post", data={"foo": "bar"}, multipart=mp)
add_part(name, *, data=/local_path=, filename=, content_type=):local_path与data二选一;data接受 str / bytes / 数字;content_type显式优先,未给时按 文件名扩展名推断(推断不出且带文件名时application/octet-stream;纯字段不带该头)。 另有Multipart.from_list([{...}, ...])批量构造、close()(纯内存对象,可选)。data=的 dict /[(k, v), ...]会作为普通字段尾随并入表单(requests 语义);multipart与json=、data=(字符串 / 字节 / 可迭代)互斥——会报错,不静默降级。content-type由构造器接管(multipart/form-data; boundary=…,同名请求头会被覆盖); 文件整块读入内存(大文件注意占用;需要边读边发的场景用既有流式上传通道自己拼 body)。
SSE(text/event-stream)
with Session(impersonate="chrome_154_windows") as s:
for ev in s.sse("https://example.com/events"): # 用的就是本会话的指纹与连接
print(ev.event, ev.data, ev.id, ev.retry)
Session.sse(url, *, method="GET", last_event_id=None, **kwargs)→ 事件生成器;Response.iter_sse()从任意已打开的响应上解析;模块级geektls.sse(url, **kwargs)走默认会话。事件是SSEEvent(event, data, id, retry)(字段名同规范)。- 协议细节按 HTML 规范:行分隔兼容
\r\n/\n/\r;空行派发(data 为空的事件不派发);data:多行以\n连接;:开头为注释(心跳);retry:数字透传;含 NUL 的id忽略。 last_event_id=会带Last-Event-ID头(断线续传);生成器结束 /break后关闭会 关闭响应。慢流场景read_timeout=(body 单次读取空闲上限)照常生效;未以空行收尾的 挂起事件在流结束时按规范丢弃。
Response
| 成员 | 类型 | 说明 |
|---|---|---|
status_code / status |
int | HTTP 状态码(status 是 0.1.0 起的旧名,等价) |
ok |
bool | < 400 为 True(requests 语义);对象真值即 ok |
reason |
str | 本地按 RFC 9110 映射的短语(非服务端原文) |
headers |
CaseInsensitiveDict |
大小写不敏感;保留插入原始大小写 |
raw_headers |
list[tuple] | 原始 (name, value) 顺序视图 |
used_protocol |
str | 实际协商到的协议:h2 / h3 / http/1.1 |
selfcheck |
dict | 本次握手的自算指纹与比对(见「自校验」) |
url / method / elapsed / encoding / apparent_encoding |
— | 请求元信息与编码推断 |
content / read() / bytes() |
bytes | 读完并缓存 body |
text |
str | 按响应编码解码(未声明 charset 时自动探测) |
json(**kwargs) |
Any | 按声明 charset(缺省 utf-8,剥 BOM)解 content;不走 text 的探测链(省掉一次"猜编码再解码") |
iter_content(chunk_size=65536, decode_unicode=False) / iter_bytes(chunk_size) / iter_lines(chunk_size, decode_unicode=False) |
生成器 | 边收边处理大响应(iter_bytes 是旧名;decode_unicode=True 跨块多字节不会切坏) |
header(name, default=None) |
str | 取单个响应头 |
cookies |
Cookies |
本响应 Set-Cookie 的解析结果(requests 同名);session.cookies.update(r.cookies) 可让它参与后续请求 |
raise_for_status() |
None | >=400 抛 HTTPError(GeekTLSError 子类,异常上挂 .response;code="http_error") |
close() |
None | 释放响应句柄(未读完 body 时更要显式调用) |
content_encoding |
str | 线上 Content-Encoding 原值(不篡改,与 requests 一致) |
decoded |
bool | 本次响应是否被引擎透明解压过 |
warnings |
list[str] | 解压层非致命提示(未知编码原样透传等) |
WebSocket(wss:// 与 ws://,RFC 6455)
wss:// 握手走同一条拨号 + TLS 指纹链路;ws://(明文)只做 TCP —— 帧层与 TLS 无关,
两条路径共用同一套帧实现,差别只在拨号那一步(与 http:// 同一条路)。
wss 的 ALPN 收窄到 http/1.1(Upgrade 在 h2 上无效;收窄的是 detail 副本里的 ALPN 扩展内容,
不只改本地偏好;明文 ws 没有 ALPN)。
Upgrade 请求头序默认对齐真 Chrome(host → connection → upgrade → origin → 身份头 →
sec-websocket-version → sec-websocket-key),profile.http1.header_order 非空时整体再过排序器。
帧层为自行实现(stdlib,无新依赖):text/binary/ping/pong/close、客户端 masking(RFC 强制)、
分片重组、控制帧 125 字节上限、ping 自动回 pong。
permessage-deflate(RFC 7692)已实现:compress=True 让握手 offer permessage-deflate,
只有对端在 101 里按我方 offer 真的接受才启用——之后 send/recv 收发的仍是明文
(RSV1 位、跨消息压缩上下文、分片续接都在库里管理)。默认 False 不发这个头:
没有逐浏览器 WS 握手的字节级证据,就不承诺 Chrome 那个 client_max_window_bits 参数
(stdlib 的压缩窗固定 15 bit)。要精确控制 offer 参数(server_no_context_takeover 等)
直接把它写进 headers。
with Session(impersonate="chrome_133") as s:
ws = s.websocket("wss://ws.postman-echo.com/raw")
ws.send("hello") # text 帧
opcode, payload = ws.recv(timeout=8) # (1, b'hello');1=text 2=binary
ws.close() # 或交给 Session 的 with 统一释放
# 想要服务端按 RFC 7692 压缩载荷(收发仍是明文):
ws = s.websocket("wss://example.com/socket", compress=True)
- 拉模型:
send(data)(str ⇒ text 帧,bytes ⇒ binary;Python 可send(data, opcode)显式指定)/recv(timeout=None, buf_size=1<<20)(秒)/close(code=1000),不做回调式。 - Node:
const ws = await session.websocket(url, { compress: true })→await ws.send(data)/ws.recvText(timeoutMs)/ws.close();asyncio 同款(AsyncSession.websocket)。 - 协商严格按 RFC,不做"看起来能用再说":对端回声我方没请求的参数、协商了不认识的扩展、 把我方压缩窗限到 <15 bit,握手直接失败;没协商却收到 RSV1(以及 RSV2/RSV3、带 RSV1 的 控制帧)也直接报错——"把压缩字节当文本返回"是绝不允许的失败模式。
- 不做 RFC 8441(WebSocket over H2):Chrome 实际仍以 H1 Upgrade 为主(capability-matrix
ws.rfc8441_h2登记)。 - 已验证:本地 echo-server 双向收发/分片/控制帧(pytest
test_websocket、nodewebsocket)+ 公网wss://ws.postman-echo.com/raw真实握手与回显;permessage-deflate 用core/engine/ws_deflate_test.go里独立写的 stdlib flate 对端(真 TLS 监听)双向验证——发方向的压缩块拼起来交给普通flate.NewReader整体解开,证明是合法连续 DEFLATE 流而非自家私有格式。
响应透明解压(默认开)
引擎按响应 Content-Encoding 自动解压 body:gzip / deflate(zlib 头 + raw flate 容错)/
br / zstd,多重编码链(如 gzip, br)按声明顺序逆序解开;全链路流式生效——
r.content / r.text / r.json() / iter_* 拿到的都是解压后的字节(charset 探测在解压之后)。
- 关闭:
Session(auto_decompress=False),或请求级request(..., auto_decompress=False)(请求级覆盖会话级)。 - headers 保留线上原值:
Content-Encoding/Content-Length不篡改;解压状态看r.decoded/r.content_encoding。 - 未知编码值原样透传不报错,记入
r.warnings。 - H3 路径同样生效(解压统一收归引擎;vendor fhttp 的内置单层解压已用
SkipResponseDecompresspatch 关闭,杜绝双重解压)。
Node.js API 参考
const { Session, version, listPresets, describePreset, checkProfile,
lastError, errorOf, GeekTLSError,
request, get, post, del, options, defaultSession } = require('geektls'); // 后五个:requests 风格模块级快捷 API
| 成员 | 说明 |
|---|---|
new Session(options) |
options 同 Python:指纹入参六选一 + proxy/proxies、proxyFromEnv/trustEnv(false = 不读 HTTPS_PROXY/ALL_PROXY)、timeoutMs/timeout、readTimeoutMs/readTimeout(秒,body 读取空闲上限)、redirectMax/allowRedirects、cookieJar、insecureSkipVerify/verify(true/false/CA bundle 路径或 PEM)、cert(路径 / [证书, 私钥] / {cert, key})+ certKey、headers、地址控制 resolve({'host[:port]': ip})/localAddress/ipVersion。也认引擎原名(timeout_ms/insecure_skip_verify/resolve…);两个名单都不在的键抛 TypeError,不会静默消失 |
s.request({method, url, ...}) |
返回 Promise<Response>(不阻塞事件循环);请求级可给 timeoutMs/readTimeoutMs/proxy,以及 allowRedirects:false(⇒ 引擎 redirect_max=-1) |
s.get/post/put/patch/delete/head/options(url, options) |
便捷动词 |
await s.websocket(url, { headers, timeoutMs, compress }) |
WebSocket(拉模型:send / recv / recvText / close);compress:true 握手 offer permessage-deflate |
s.close() |
释放会话 |
r.statusCode / r.ok / r.reason / r.headers / r.usedProtocol / r.selfcheck |
同 Python 语义 |
r.contentEncoding / r.decoded / r.warnings |
解压元信息(同 Python) |
await r.read() / r.bytes() / r.text() / r.json() / r.header(name) |
body 读取 |
r.iterContent(size) / r.iterLines(size) |
异步迭代器(for await) |
r.close() |
释放 |
version() / init() / lastError() / listPresets() / describePreset(name) / checkProfile(input) |
与 Python 同名同语义 |
errorOf(handle) |
按对象取最近一次错误(gtls_error_of;handle 查不到再回落 lastError())。异步绑定在 worker 线程报的错靠它取回 |
| cookie | Node 侧没有 Python 那样的显式 jar API:线上 Set-Cookie 的存取与重发由引擎 jar 自动完成(含重定向中间跳);要带指定 cookie 就写 Cookie 头 |
Go API 参考
两条路:绑定层(github.com/geekbyter/geektls/bindings/golang,http.Client/RoundTripper 形态)与引擎直连(core/engine,能力最全)。
| 包 | 关键 API | 说明 |
|---|---|---|
core/profiles |
List() / Get(name) / Describe(name) / FromJA3 / FromJA4R / FromClientHelloHex / NormalizeForReplay / Parse |
预设与指纹入口(含上面说的自洽归一) |
core/tls |
CompileDetail / ComputeJA3 / ComputeJA4 / JA3Hash / CheckProfile / ResolveJA4Preset |
编译、自算、离线自检、JA4 反查 |
core/engine |
NewSession(profile, SessionOptions) → Do(*Request) → *Response(StatusCode/OK/Reason/Header/Text/JSON/IterBytes/Close);DialWS(*WSRequest) → *WSConn(Send/Recv/Close,WSRequest.Compress 开 permessage-deflate) |
引擎直连;支持 H2/H3 racing、Alt-Svc、代理、流式、wss |
github.com/geekbyter/geektls/bindings/golang(包名 geektls) |
NewSession(preset, *Options) → Do/Get / DialWS(*WSRequest) → *WSConn / Close();NewRoundTripper(preset, *Options) |
绑定层(不走 FFI,直接 import core);WSRequest/WSConn 是 engine 类型的别名,不再包一层以免漂移 |
core/ffi |
gtls_* C ABI(见 core/ffi/geektls.h) |
Python/Node 绑定的底座;gtls_client_new 的 config_json 支持 impersonate/profile/ja3/ja4/ja4r/clienthello_hex |
使用场景
1) 单请求(最常用)
with Session(impersonate="chrome_154_windows") as s:
print(s.get("https://tls.peet.ws/api/all").json()["tls"]["ja4"])
2) 并发抓取(会话复用 + 连接池)
import concurrent.futures as cf
with Session(impersonate="chrome_150") as s:
with cf.ThreadPoolExecutor(16) as ex:
for r in ex.map(lambda u: s.get(u), urls):
print(r.status_code, r.used_protocol)
3) 会话复用/票据(第二次握手带 PSK,selfcheck 里能看到)
with Session(impersonate="firefox_156_windows") as s:
s.get("https://example.com").close()
r = s.get("https://example.com") # resumed(同 origin 连接复用)
4) 强制 HTTP/3 / H3 竞速
with Session(profile=geektls.describe_preset("chrome_154_macos")) as s:
r = s.get("https://cloudflare.com", force_http3=True, timeout_ms=8000)
print(r.used_protocol) # h3(不支持则按策略回落 h2)
5) 走代理 / 关校验 / 不跟随重定向
with Session(impersonate="edge_153_windows", proxies={"https": "http://127.0.0.1:8080"},
verify=False, allow_redirects=False) as s:
print(s.get("https://example.com").status_code) # 3xx 原样返回
代理支持六种 scheme:http/https(CONNECT 隧道,带 user:pass 即发
Proxy-Authorization)、socks5(本地先解析域名再交代 IP)、socks5h
(域名原样交给代理解析)、socks4/socks4a(老协议:只有 IPv4 目标、
鉴位只有 user id 没有口令;4a 的域名由代理侧解析)。
socks5 遇到解析不出的域名会直接报错,不会静默降级成 socks5h。
不给 proxies 时读标准环境变量 HTTPS_PROXY → ALL_PROXY(大小写都认),并按
NO_PROXY 豁免;trust_env=False 可整条关掉(指纹调试要可复计时建议关)。
域名是字面量匹配,不做 DNS——真 Chrome 会先解析再判 NO_PROXY,geektls 故意不这么
做:为的是"同一个预设、同一个 URL 每次走同一条路"。
回环目标(localhost / 127.0.0.1 / ::1)不吃环境变量派生的代理,避免开发机
上挂了 ALL_PROXY 就连不上本地测试服务;显式给的 proxies 对回环仍然生效。
⚠️ 有代理时 H3/QUIC 一律不参与(含 racing 与 Alt-Svc 升级):QUIC 过 HTTP 代理 需要 CONNECT-UDP(RFC 9298),本库未实现。
force_http3=True+ 代理会明确报 invalid 而不是偷偷直发漏指纹——这是"要么按你说的路走、要么报错"的一般原则。
5b) 自持信任库 / mTLS(requests 的 verify=<path>、cert=(crt, key))
with Session(impersonate="chrome_150",
verify="/etc/ssl/certs/internal-ca.pem", # 路径 / 目录 / PEM 文本皆可
cert=("/etc/client/tls.crt", "/etc/client/tls.key")) as s:
print(s.get("https://internal.example.com/api").status_code)
CA 解析不出来、或证书与私钥配不上对时,建会话就报错(GeekTLSError,
code=invalid_config),不会退化成"用系统根继续"。TCP(uTLS)与 QUIC/H3 内层
握手共用同一份证书材料。
5c) 不走系统 DNS:把域名钉到固定 IP / 绑源地址 / 限协议族
with Session(impersonate="chrome_150",
resolve={"api.example.com": "93.184.216.34", # 键也可以是 "host:443"
"cdn.example.com": "104.16.132.249"},
local_address="192.168.1.23", # 出网走这张网卡/这个 IP
ip_version="4") as s: # 只要 A 记录
print(s.get("https://api.example.com/v1").selfcheck["ja4"])
这三项只改"连到哪、从哪连",一律不改线上指纹字节:SNI、Host、伪头、
ClientHello 用的还是 URL 里的原域名,所以钉位前后 JA3/JA4 完全一致(有测试守着)。
local_address 必须是本机持有的 IP——绑一个不属于它的地址会在拨号时明确报错,
不会"静默换一张网卡"。
和 netstack 组合时它是正向能力:用户态栈要求目标是 IPv4 字面量,resolve 正好
供给;源 IP 由 ip_version/resolve 的族隐含。
生效范围按"谁做解析"划分,不做静默改道:直连、tcp.mode=netstack、socks5、
socks4 由引擎本地解析 ⇒ 钉位/族收窄都生效;socks5h、socks4a、CONNECT 隧道把
域名原样交给代理 ⇒ resolve/ip_version 不参与(那两档生效就等于绕过代理语义)。
local_address 在 netstack 下报错(用户态栈源地址固定为 TUN 侧地址)而不是忽略。
取值互相冲突(ip_version="4" 配 IPv6 源地址、resolve 值不是 IP、键写成网卡名)
建会话即 invalid_config;force_http3 与三者同给也是明确报错——QUIC 拨号没接地址
控制:配置不生效时直接失败,不会静默走别的路。
6) 对比两个预设的指纹差异(不发包)
import json
a = geektls.check_profile(json.dumps(geektls.describe_preset("chrome_152_macos")))
b = geektls.check_profile(json.dumps(geektls.describe_preset("chrome_154_macos")))
print(a["ja4"], b["ja4"], a["wire_len"], b["wire_len"])
7) 自定义完整指纹(自己的 profile JSON)
mine = {
"name": "mine_chrome_like",
"tls": {"detail": {
"legacy_version": "0x0303",
"ciphers": ["grease", "0x1301", "0x1302", "0x1303", "0xc02b", "0xc02f"],
"extensions": [
{"type": 0, "sni": "auto"},
{"type": 16, "alpn": ["h2", "http/1.1"]},
{"type": 43, "versions": ["grease", "0x0304", "0x0303"]},
{"type": 10, "groups": ["grease", "X25519MLKEM768", "X25519"]},
{"type": 13, "sig_algs": ["0x0403", "0x0804", "0x0401"]},
{"type": 41}, # 空占位:会话复用要求它在末尾
],
}},
"http2": {"hpack_strategy": "chrome", "settings": [[1, 65536], [2, 0], [4, 6291456], [6, 262144]],
"window_update": 15663105, # 三态:不写=补默认 15663105 / 0=不发这帧 / N=发 N
"first_stream_id": 1, # 客户端首流号(正奇数);不写=1,真 Firefox 用 3
"pseudo_header_order": ["m", "a", "s", "p"]},
}
geektls.check_profile(json.dumps(mine)) # 先离线自检:看 warnings 与 JA3/JA4
with Session(profile=mine) as s:
print(s.get("https://example.com").selfcheck["ja4"])
8) 从抓包迁移:JA3 / JA4R / 原始 hex
with Session(ja3="771,4865-4866-4867,43-10-11-13-16-23-5-0-65281,4588-29-23-24,0") as s:
r = s.get("https://example.com") # 其余字段按引擎默认补齐
r = geektls.check_profile("t13d1516h2_8daaf6152771_d8a2da3f94cd") # 反查内置预设
9) 抓包 → 预设(把线上真实指纹固化成文件)
import json, pathlib
r = Session(impersonate="chrome_150").get("https://tls.peet.ws/api/all")
cap = r.json()
spec = { # 只保留指纹相关字段(tcp 节按宿主平台另配)
"name": "my_captured_profile",
"tls": {"detail": {"legacy_version": "0x0303", "ciphers": ["grease", "0x1301"], "extensions": []}},
"http2": {"akamai": cap["http2"]["akamai_fingerprint"]},
}
pathlib.Path("my_profile.json").write_text(json.dumps(spec, ensure_ascii=False, indent=2))
更规范的做法是走仓库的
tests/e2e/cmd/gen-profiles(从原始 ClientHello 字节生成,落core/profiles/builtin/并带证据等级)。
10) 回归/自校验
JA4 = "t13d1517h2_8daaf6152771_cb7bf5808d99"
with Session(impersonate="chrome_154_windows") as s:
assert s.get("https://example.com").selfcheck["ja4"] == JA4
预设体系
- 数量与命名:368 条,
core/profiles/builtin/<name>.json,文件名即预设名,规范是<家族>_<版本>[_<变体>][_<平台>]:chrome_154_windows、firefox_156_android、okhttp_3_12_12。- 平台后缀能判定就必须写(windows/macos/linux/android/ios),判不定的留空(工具/App 族);
- Safari 也带
_macos——它同时有 macOS 与 iOS 形态(safari_18_6_ios),后缀只允许_macos/_ios(已按此把safari_16→safari_16_macos、safari_18→safari_18_legacy_macos); - 旧名是别名:改名过的预设(
chrome_131/133/150、firefox_120/135→*_windows;Safari 两条同上)旧名继续可用, 取到同一份形态;映射表只增不删,新代码用规范名。 - 仍然没有族级短名/前缀匹配:
impersonate="chrome_154"会报preset not found,要写全名。 完整规范与理由见 docs/03-profile-format.md §3.1(测试core/profiles/naming_test.go)。
- 字段:
tls.detail(ciphers/扩展与负载/GREASE 策略)、http2(SETTINGS/window_update(三态)/伪头序/first_stream_id/hpack_strategy/headers_priority)、http3(transport params/inner hello 规约)、identity(导航头集合与顺序)、可选tcp、grade、source。 - 证据分级(
grade字段,详见 docs/09-alignment-and-superiority.md):- 留空 = 本项目自测(E1 真机抓包 / E1r 字段级实测 / E2 本机可复现)→ 参与 E1 级断言;
E2i/E2i-u= 谱系内插(实测锚点 + 逐字段稳定性规则,-u表示区间内有变化且边界未知);E3= 外部指纹集导入(只补覆盖,不做保真承诺)。- 4 个 E1 级测试(fp oracle / Chromium GT / Firefox GT / TLS1.2 回退)一律跳过
grade != ""的预设——没实测的不参与"实测"断言。
- 怎么挑:要稳就挑
grade为空、且与目标族/版本/平台都对的(describe_preset()看全貌);只求覆盖面可用E3。 - 怎么加:见 docs/maintenance.md 的"预设入库流程"。由
gen-profiles生成的预设禁止手改(CI 逐字节比对校验)。
自校验
每个响应带 selfcheck:实际发出 ClientHello 的自算 JA3/JA4 + JA3 fullstring、线上扩展序(含/剔 GREASE 双份)、GREASE 值与位置、协商 cipher/版本/ALPN、SNI 是否上链(IP 目标自动报 i);JA3/JA4R 入口还含期望值比对;gtls_check_profile 支持 profile JSON / JA3 / JA4 / JA4R / hex 五入参离线自检。
身份层也有对应校验(core/profiles/builtin_identity_test.go):任何预设都不得带 headless 令牌,且 UA 与 sec-ch-ua 的平台/版本必须自洽(Windows NT ↔ "Windows"、Macintosh ↔ "macOS"、Chrome/Edg 主版本 ↔ "Chromium";v="N")。
性能摘要
- FFI 调用开销:
gtls_version6–7µs、gtls_check_profile26–30µs(<50µs 目标达成) - 回环吞吐(连接池开启,并发 100,10s):共享 Session(同 origin 单连接,浏览器真实形态)≈5–9k req/s;每 worker 会话模型 Go 12.7k / Python 9.4k / Node 8.9k req/s
- 冷启动:Session 初始化 0.2ms、首请求 ~3.5ms
与同类项目对比
对比依据(2026-09-29 复核):逐仓拉取下列项目 README 原文核对能力声明,结合
tls-spoofing-library-matrix 与 docs/10-ecosystem-comparison.md。
三点口径:① akamai/uls 实为 Akamai 的日志流 SIEM 工具,不是指纹库,已从对比剔除
(此前表格误列);② cyCronet 的地址是 2833844911/cyCronet
(Python + Chromium Cronet 真栈)——此前按名字猜 slug(cycronet/cycronet)查不到就写"仓库不存在",
是错的:查不到 ≠ 不存在,已补回对比行;③ bogdanfinn/tls-client 的 README 本次拉取失败
(raw 与镜像均 404),其能力按既有文档与本项目 E3 导入记录描述。单元格 — 表示公开材料未说明
该维度(不等于没有)。
完整客户端(TLS / H2 / H3 伪装客户端)
| 项目 | 栈 / 语言 | TLS 指纹 | H2 帧 + 头序 | H2 HPACK 策略 | H3 / QUIC | 四层 TCP | WebSocket | 预设与证据 | 响应内自校验 |
|---|---|---|---|---|---|---|---|---|---|
| geektls | Go(c-shared)+ Python / Node / Go 绑定 | ✅ uTLS fork + E1 真机采集链路 | ✅ | ✅ 四档(generic/chrome/firefox/safari),255/394 预设带值 | ✅ 内层 ClientHello 同 profile + transport params blob 直通;Initial 布局不可控(待 QUIC 栈内化后解锁) | ✅ TTL/MSS/DF/window/wscale:setsockopt 三平台 + netstack 档(Linux root,gVisor 栈) | ✅ RFC 6455 + permessage-deflate(握手走指纹链路) | ✅ 394 条 grade/source 分级:29 自测 / 6 E2i / 8 E2i-u / 351 E3 |
✅ selfcheck + check_profile 五入参 |
bogdanfinn/tls-client(hrequests、noble-tls、tls-client-sharp 等绑定) |
Go fhttp + utls |
✅ | ✅ | — | ✅ | — | — | 自带 profile 集;与本项目 E3 覆盖(320 条)无直接来源关系——E3 的 source 逐条指向第三方快照 profiles/evidence/thirdparty/tls_config-0.0.2(TestE3SourceTraceable 校验),profile 组织形态是同类参照 |
— |
lexiforest/curl_cffi(活跃)/ lwthiker/curl-impersonate(原始) |
libcurl 补丁 + BoringSSL / NSS | ✅ | ✅ | — | ✅(curl_cffi 新版起) | ❌(libcurl 无 TCP 指纹面) | ✅ | 内置画像 + 自定义指纹;社区节奏最快 | — |
Danny-Dasilva/CycleTLS、cycletls_python |
Go utls + fhttp + quic-go |
✅(JA3 可配置) | ✅(fhttp 头序) | — | ✅ | — | ✅ | profile 清单;socks4/5/5h | — |
sardanioss/httpcloak |
Rust 栈,Python/JS/C# 绑定 | ✅ | ✅(WINDOW_UPDATE/priority 可调) | 部分(priority 侧) | ✅(H3 GREASE 帧) | ✅(TTL/MSS/Window,README 声明) | — | 内置 preset + "抓一次用到底"(JA3+Akamai 粘贴导入) | — |
jaredboynton/specter |
Rust + 自研 H2(RFC 9113)/ H3 | ✅ Chrome 142–148、Firefox 133–151+ESR | ✅(记录分帧/帧时序/池行为) | — | ✅ | — | ✅ | 内置 profile + UA-CH 算法文档 | — |
zhkl0228/impersonator |
Java / kwik(QUIC) | ✅ | ✅ OkHttp 形态 | — | ✅ 含 QUIC Initial 布局(分片/乱序/PADDING 按浏览器拟真) | — | — | — | — |
sqdshguy/wreq-js |
Node(Rust wreq 内核) | ✅ | ✅ Akamai | — | — | — | ✅(WHATWG 风格 + 会话复用) | Chrome 至 149 / Firefox 151,声明"live capture 验证" | — |
daijro/hrequests |
Python(tls-client 内核) | ✅ | ✅ | — | — | — | — | requests 风格 + BrowserForge 头生成 + 浏览器自动化 | — |
2833844911/cyCronet |
Python + Chromium Cronet 真栈(libcronet.so) |
✅ 真栈(默认 chrome_144,tls_profiles.json 可自定义) |
✅ 真栈 | —(真栈内置,策略不可控) | —(README 未见 H3/QUIC) | ❌ | ✅(WSS,TLS 指纹一致) | chrome_144 + 自定义 json;SOCKS5 带鉴权 |
❌(无自校验回读) |
deedy5/primp、wangluozhe/requests-go、z402166914/pyhttpx、tocha688/curl-cffi-node |
各语言封装 | ✅ 依赖上游 | ✅ 依赖上游(requests-go 另有 JA3 扩展随机化) | — | 部分 | — | 部分 | — | — |
thesatellite-ai/fetchr |
Go + CLI + MCP server | ✅(JA3/JA4) | ✅ | — | ✅ | — | — | 浏览器画像 | — |
采集 / 检测 / 代理(对照面,不是竞品)
| 项目 | 作用 | 与 geektls 的关系 |
|---|---|---|
gospider007/fp |
独立 ClientHello 解析器 | E1 采集链路直接使用(tests/e2e/cmd/e1-browser) |
FoxIO-LLC/ja4、Crank-Git/ja4plus-go、XOR-op/ja-tools |
JA3 / JA4 / JA4+ 参考实现 | JA4 规范与测试向量来源;只依赖 JA4 本体(JA4+ 部分方法有许可证限制) |
wi1dcard/fingerproxy、LyleMi/ja3proxy、O-X-L/haproxy-ja4-fingerprint |
反向代理 / 网关算指纹并透传 | 输出形态对照;selfcheck 做在响应里,不依赖网关 |
jaeles-project/gospider、gitee baixudong/gospider、Ecalose/gospider |
爬虫 / 工具链(同名不同项目) | 爬虫侧用法参照 |
tlsmask/tlsmask、Easonliuliang/helloprint |
指纹伪装 / 一致性工具 | UA ↔ TLS 一致性思路对照;落成身份层测试 |
上表里 geektls 的差异点
- 四层一起:TLS + H2(含 HPACK 编码策略四档)+ H3 / QUIC(内层 ClientHello 同 profile)+ TCP(TTL/MSS/DF/window/wscale:setsockopt 三平台 + Linux netstack 档)。上表其他客户端多数止步 TLS / H2;四层 TCP 只有 httpcloak 声明了同档能力。
- 证据分级:
grade(30 自测 / 6 E2i / 8 E2i-u / 320 E3)+source溯源校验 + E1 真机采集链路 + 语料回归 + 外部 oracle 周检;同类普遍只给一份清单,不区分实测与转写。 - 响应内自校验:本次握手实际发出的 JA3 / JA4 / 扩展序 / GREASE 值直接从响应取,可当回归断言;
check_profile支持五种入参离线自检。 - 三语言同引擎:Python / Node / Go 共用同一 C ABI,跨语言 JA4 三方一致,不需要为每种语言重写指纹栈。
- 预设结构:394 条按
grade分层(29 自测可参与严格断言 + 6 E2i / 8 E2i-u 谱系内插 + 351 E3 导入),覆盖浏览器 / App / 工具 / 代理等 29 族,含跨平台同版本一致性断言(chrome_152/chrome_154在 macOS / Android / Windows 上 JA4 逐字符相同)。 - 部署形态:Go 实现、CGO 只用于构建动态库,产物是单文件动态库 + 平台 wheel,无需 libcurl 补丁链。
- 自主化路线:指纹相关代码路径正向 100% 自有推进(uTLS / fhttp / quic-go-utls 内化裁枝四阶段,见 docs/plans/2026-09-29-self-contained-roadmap.md);密码学原语(circl / brotli / zstd)按行业共识保留成熟实现,不自写。
- 能力面补齐:WebSocket(wss / ws,握手走指纹链路)+ 明文
http://+ Python asyncio + 四编码自动解压 + 命令行入口 + requests 语义面(cookie jar / timeout 元组 / auth / 重定向开关 / 表单编码),与 CycleTLS / curl_cffi / noble-tls 的能力清单逐项对齐(逐库对照见 docs/10-ecosystem-comparison.md)。
T-HPACK 四档的证据来源:chrome 档对齐 Chromium QUICHE HpackEncoder 的默认策略(源码级),
firefox 档对齐 Firefox 59 抓包(字节级),safari 档为保守近似(待 E1 校验);上游 x/net 编码器
"一切皆可入动表"的行为与前三者都不同。
相对短板(同一张表下的客观差距)
| 维度 | 差距 |
|---|---|
| 生态与熟悉度 | 远不如 curl_cffi / curl-impersonate:Python 迁移成本、示例与社区最少;没有 hrequests 那种浏览器自动化集成 |
| 真栈行为级 | curl_cffi(libcurl + BoringSSL)与 cyCronet(Chromium Cronet 真栈)的行为级细节天然全真(含握手重试、协议栈内部实现);这里靠逐维度对齐逼近,理论上存在未发现的行为差异 |
| 预设更新 | 依赖人工采样(SLA 2 周);大量预设是 E3 转写而非实测,curl_cffi / curl-impersonate 靠社区补丁更快 |
| H3 可控面 | QUIC 首飞全控:首 datagram 填充下限(initial_packet_size,1200–1452,不设=1200)、SCID 长度(connection_id_length,0 = Chrome 形态空 SCID)、PADDING 位置、CRYPTO 分片表、scrambling 开关、coalesce 阈值(initial_layout,vendor patch #8/#10);transport params 顺序靠 blob 直通;Firefox / Safari H3 未实测 |
| Safari 侧 | HPACK 的 safari 档是全 literal 保守近似;padding 用实测字节数表达;无 CFNetwork 字节级证据 |
| 四层 | Windows 仅 setsockopt 档(TTL/MSS/DF,MSS 项缺失);netstack 档(window/wscale 精确控制)仅 Linux root;IP ID / TSval 不可控 |
| 发布面 | npm 与 Go module 尚未发布;wheel 只覆盖 5 平台(无 musllinux / Windows ARM64) |
| 依赖 fork | fhttp、quic-go-utls 是 vendor fork,升级上游要重打 patch 并跑回归(流程已登记,仍是维护成本) |
| 有损入口 | JA3 / JA4 入参天然丢负载,此处选择明确告警、而不是伪装保真,在只求"JA3 过检测"的场景不是最省事的路径 |
自主化进度(docs/plans/2026-09-29-self-contained-roadmap.md):
边界声明与依赖清单已完成;gvisor.dev/gvisor(netstack 档)已引入;core/internal/ 目前只有
registry ⇒ 自主化三项(uTLS 内化改写 / fhttp 裁枝内化 / quic-go-utls 内化)均未开始,
"指纹路径 100% 自有"的终态尚未达成。不变量:ABI 签名只增不改、394 预设指纹输出逐比特不变、
每阶段全量回归 + L2 nginx 终审。
能力对齐现状与追赶排期(差距清单 + 关闭判据 + 明确不做的事)见
docs/plans/2026-09-29-parity-and-improvement-plan.md:
指纹能力面已对等或领先。形态面已补齐(明文 http:// / ws://、CLI、cookie 手感、
requests 语义面);剩余差距集中在发布面(Node/Go 未发布、平台不全)与两条结构性差距
(QUIC Initial 的 coalesce / 分片表属上述第三项,真栈行为级无法追平)。
已知边界与不足
| 边界 | 说明 |
|---|---|
| QUIC 内层 ECH 与 bogdanfinn 服务端 | 其 QUIC server 不处理 ECH-in-QUIC 会静默失败(客户端侧已合成 Chrome 等效 payload;真服务器按规范忽略 GREASE ECH) |
| QUIC Initial 布局 | 已全控(vendor patch #8/#10):PADDING 位置、CRYPTO 分片表、scrambling 开关、coalesce 阈值(initial_layout)、首包填充下限(initial_packet_size,不设=1200,不是"精确尺寸")、首飞 SCID 长度(connection_id_length,0 = Chrome 形态空 SCID;此时 initial_source_connection_id 才允许空值)。逐字节复刻真机首包 1230B 还需 CH 尺寸与分片表一致(属 profile 侧保真,不在布局旋钮内) |
明文 http:// 的边界 |
只走 H1(不做 h2c,无 prior knowledge / Upgrade 路径);经 HTTP 代理时走 CONNECT 隧道(不发 absolute-form GET http://…);明文没有 TLS 层 ⇒ selfcheck 恒为零值、force_http3 + http:// 直接报错 |
| 代理环境变量按 scheme 取 | https/wss 读 HTTPS_PROXY、http/ws 读 HTTP_PROXY,都不跨 scheme 取(curl/requests 同语义);都没命中再退 ALL_PROXY |
| QUIC transport params 顺序/非标参数 | 经 transport params blob 直通可控(顺序与值均有断言);个别残余值与 blob 整块替换冲突,待 fork 决策 |
| Windows TCP MSS | 不支持(WSAENOPROTOOPT 实测),跳过+warning;raw socket 档仅 Linux 探测模式 |
| uTLS PSK 复用 vs Go std 服务端 | binder 校验失败(上游 interop,发出侧已证正确) |
| JA4 短哈希入参 | 哈希不可逆 ⇒ 只能反查内置预设;想精确复刻必须给 JA4R 或完整 profile |
| nginx L2 终审 | ✅ 已通过(WSL2,35/35);JA4TCP 验收仍需真实二层网络 |
开发与发布
make build # 构建动态库到 build/
make cli # 构建命令行入口到 build/geektls
make smoke # Go/Python/Node 三语言冒烟
cd core && go test ./... # 引擎全量(含 E1/E2 门禁、语料回归)
cd tests/e2e && go test ./... # e2e:生成器校验、HPACK/H2 回放、loopback、跨语言
GEEKTLS_ORACLE_PRESETS=chrome_154_* GEEKTLS_ORACLE_ASSERT=1 \
go test -tags external -run Oracle -v # 外部 oracle 对拍(子集 + 不一致即失败)
wsl -d Ubuntu -- bash scripts/wsl-test.sh # Linux 侧验证(WSL,Go 自动装到 ~/.local)
- 发布:
docs/versioning.md(语义化 + ABI 规则 + 三处版本同步);PyPI 流水线.github/workflows/release-pypi.yml(五平台 wheel)、CI.github/workflows/ci.yml(含"预设必须等于生成器产物"逐字节校验)。 - nightly 实测:
.github/workflows/oracle-nightly.yml— 外部 oracle(JA3/JA4 + Akamai H2)以断言档跑选定子集、ECH 与真机会话恢复(resumed=true是硬门禁)、Go/Python/Node 三语言一致性与两套绑定 e2e(Linux + Windows 真构建动态库)。子集由GEEKTLS_ORACLE_PRESETS控制,手动 dispatch 可临时加新预设;每周另有.github/workflows/preset-freshness.yml比对内置预设与 Chrome/Firefox 官方渠道版本,落后即开 issue。 - 预设维护:
docs/maintenance.md(Chrome 2 周 SLA、入库五步、oracle 周检、fork rebase)。
许可证
本项目自身按 MIT 授权(见 LICENSE,2026-09-29 定)。
发布物(wheel / npm 包 / Go module / 编译进动态库的依赖)内含第三方代码,各依赖的许可证与
归属声明登记在 LICENSES.md 并随 wheel 一起分发:全量闭包审计无 GPL/LGPL;
两个已知注意项:上游 fhttp 无显式 LICENSE(仅"无授权使用"风险,行业生态同此依赖)
与 JA4+ 的许可证/商标边界(只依赖 JA4 本体,见上文对比表)。
文档索引
非目标
- 不做服务端指纹采集。
- 不做浏览器级 JS 环境模拟(Canvas/WebGL 等)。
- 不内置任何绕过具体站点防护的"开箱即用"策略——只提供精确的指纹原语。
Metadata
Release files for geektls 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| geektls-0.2.1-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| geektls-0.2.1-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| geektls-0.2.1-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| geektls-0.2.1-py3-none-macosx_11_0_x86_64.whl | Python 3 | none | macOS 11.0+ x86-64 | Details |
| geektls-0.2.1-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
Total release size: 25.5 MB
Release files / geektls-0.2.1-py3-none-win_amd64.whl
| Download URL | geektls-0.2.1-py3-none-win_amd64.whl |
|---|---|
| Size | 4.9 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
e2933ea3477c85a7263c618c5103ff7f027187cefb827936d8ff3d6499a172a7
|
|
BLAKE2b-256 checksum How to use checksums |
cf95dc2bd95e507a661826a5d3dcd3b09e76d49488d8d546270bc05b15f2a463
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / geektls-0.2.1-py3-none-manylinux_2_28_x86_64.whl
| Download URL | geektls-0.2.1-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 5.9 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
306b95a4d5d6b3979dc5f90220f06fcfe7007f788938878640ab17f34c6262b1
|
|
BLAKE2b-256 checksum How to use checksums |
60ccbc94bc6f1b37076f335214f811b3ef3d7790d74a4ca03165c06d0f21400e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / geektls-0.2.1-py3-none-manylinux_2_28_aarch64.whl
| Download URL | geektls-0.2.1-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 5.4 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
0a01fd196ded754e85c7b1d252a8260e868e1f023c7c1b3b90d386e9a78e5a84
|
|
BLAKE2b-256 checksum How to use checksums |
728e9dfb43a170d49c1c5b95d19ed7f42c5c616aa8886132efb9371381f3b268
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / geektls-0.2.1-py3-none-macosx_11_0_x86_64.whl
| Download URL | geektls-0.2.1-py3-none-macosx_11_0_x86_64.whl |
|---|---|
| Size | 4.8 MB |
| Tags | Python 3 macOS 11.0+ x86-64 |
|
SHA-256 checksum How to use checksums |
7bd25c19b48f9430533bce355a86c5c447d43cec2c203bbe71f935c2e43e8cdb
|
|
BLAKE2b-256 checksum How to use checksums |
45c069ff76bcfb00c2c0818bdb40d2d3ec0002ccadd28763a64c026f655b96ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / geektls-0.2.1-py3-none-macosx_11_0_arm64.whl
| Download URL | geektls-0.2.1-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 4.4 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
cba8f8f3ada32d4f5cfd519d055c2a586911f0f3e8cb5a5359224a8266c5a98e
|
|
BLAKE2b-256 checksum How to use checksums |
75f045a6038a44b9ce165edc0851f0178dcd34e8e8e69072609a32f37e1d5df8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|