tls-client-python 1.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tls_client/__init__.py +43 -0
- tls_client/_core.py +2046 -0
- tls_client/bin/tls-client-alpine-amd64.so +0 -0
- tls_client/bin/tls-client-darwin-amd64.dylib +0 -0
- tls_client/bin/tls-client-darwin-arm64.dylib +0 -0
- tls_client/bin/tls-client-linux-386.so +0 -0
- tls_client/bin/tls-client-linux-amd64.so +0 -0
- tls_client/bin/tls-client-linux-arm.so +0 -0
- tls_client/bin/tls-client-linux-arm64.so +0 -0
- tls_client/bin/tls-client-windows-386.dll +0 -0
- tls_client/bin/tls-client-windows-amd64.dll +0 -0
- tls_client_python-1.14.0.dist-info/METADATA +384 -0
- tls_client_python-1.14.0.dist-info/RECORD +16 -0
- tls_client_python-1.14.0.dist-info/WHEEL +5 -0
- tls_client_python-1.14.0.dist-info/licenses/LICENSE +27 -0
- tls_client_python-1.14.0.dist-info/top_level.txt +1 -0
tls_client/_core.py
ADDED
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
tls_client._core – Low‑level CFFI binding for the Go tls-client engine.
|
|
4
|
+
|
|
5
|
+
Architecture
|
|
6
|
+
────────────
|
|
7
|
+
• Go shared library compiled with `-buildmode=c-shared` exports four
|
|
8
|
+
symbols: ExecuteRequest, RequestAsync, FreeResponse, ClearClientPool.
|
|
9
|
+
• Sync path: ExecuteRequest blocks until completion, returns CResponseResult*.
|
|
10
|
+
Python wraps it with `raw_res = ffi.gc(raw_res, lib.FreeResponse)` for
|
|
11
|
+
automatic cleanup.
|
|
12
|
+
• Async path: RequestAsync deep-copies all C data into Go heap, dispatches a
|
|
13
|
+
goroutine, and returns immediately. The goroutine executes the HTTP request
|
|
14
|
+
and calls a CFFI callback with (request_id, CResponseResult*). Python
|
|
15
|
+
unpacks the response in the callback, calls FreeResponse, and resolves an
|
|
16
|
+
asyncio.Future via `loop.call_soon_threadsafe()`. No thread pool — Python's
|
|
17
|
+
event loop is never blocked.
|
|
18
|
+
• Response body is read via `ffi.buffer(res.body, res.body_len)[:]` for
|
|
19
|
+
a single zero-copy-style memory view → Python bytes conversion.
|
|
20
|
+
• All temporary C allocations are anchored in a `keep_alive` list for the
|
|
21
|
+
duration of the call, preventing premature GC and dangling pointers.
|
|
22
|
+
|
|
23
|
+
Classes
|
|
24
|
+
───────
|
|
25
|
+
• Session – synchronous client. Supports `with` context manager.
|
|
26
|
+
• AsyncSession – asynchronous client using Go goroutines + CFFI callbacks.
|
|
27
|
+
Compatible with uvloop / winloop for maximum event-loop performance.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
import ctypes
|
|
34
|
+
import os
|
|
35
|
+
import platform
|
|
36
|
+
import sys
|
|
37
|
+
import threading
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any, ClassVar, Dict, List, Optional, TypedDict
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Supported TLS client identifiers — mirrored from profiles/profiles.go
|
|
43
|
+
# Exported so users can introspect: from tls_client import SUPPORTED_IDENTIFIERS
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
SUPPORTED_CLIENT_IDENTIFIERS: Dict[str, List[str]] = {
|
|
47
|
+
"Chrome": [
|
|
48
|
+
"chrome_103", "chrome_104", "chrome_105", "chrome_106",
|
|
49
|
+
"chrome_107", "chrome_108", "chrome_109", "chrome_110",
|
|
50
|
+
"chrome_111", "chrome_112",
|
|
51
|
+
"chrome_116_PSK", "chrome_116_PSK_PQ",
|
|
52
|
+
"chrome_117", "chrome_120", "chrome_124",
|
|
53
|
+
"chrome_130_PSK",
|
|
54
|
+
"chrome_131", "chrome_131_PSK",
|
|
55
|
+
"chrome_133", "chrome_133_PSK",
|
|
56
|
+
"chrome_144", "chrome_144_PSK",
|
|
57
|
+
"chrome_146", "chrome_146_PSK",
|
|
58
|
+
],
|
|
59
|
+
"Firefox": [
|
|
60
|
+
"firefox_102", "firefox_104", "firefox_105", "firefox_106",
|
|
61
|
+
"firefox_108", "firefox_110",
|
|
62
|
+
"firefox_117", "firefox_120", "firefox_123",
|
|
63
|
+
"firefox_132", "firefox_133", "firefox_135",
|
|
64
|
+
"firefox_146_PSK",
|
|
65
|
+
"firefox_147", "firefox_147_PSK",
|
|
66
|
+
"firefox_148",
|
|
67
|
+
],
|
|
68
|
+
"Safari": [
|
|
69
|
+
"safari_15_6_1", "safari_16_0",
|
|
70
|
+
"safari_ipad_15_6",
|
|
71
|
+
"safari_ios_15_5", "safari_ios_15_6", "safari_ios_16_0",
|
|
72
|
+
"safari_ios_17_0",
|
|
73
|
+
"safari_ios_18_0", "safari_ios_18_5",
|
|
74
|
+
"safari_ios_26_0",
|
|
75
|
+
],
|
|
76
|
+
"Brave": [
|
|
77
|
+
"brave_146", "brave_146_PSK",
|
|
78
|
+
],
|
|
79
|
+
"Opera": [
|
|
80
|
+
"opera_89", "opera_90", "opera_91",
|
|
81
|
+
],
|
|
82
|
+
"OkHttp (Android)": [
|
|
83
|
+
"okhttp4_android_7", "okhttp4_android_8", "okhttp4_android_9",
|
|
84
|
+
"okhttp4_android_10", "okhttp4_android_11",
|
|
85
|
+
"okhttp4_android_12", "okhttp4_android_13",
|
|
86
|
+
],
|
|
87
|
+
"Mobile / App SDKs": [
|
|
88
|
+
"zalando_android_mobile", "zalando_ios_mobile",
|
|
89
|
+
"nike_ios_mobile", "nike_android_mobile",
|
|
90
|
+
"mms_ios", "mms_ios_1", "mms_ios_2", "mms_ios_3",
|
|
91
|
+
"mesh_ios", "mesh_ios_1", "mesh_ios_2",
|
|
92
|
+
"mesh_android", "mesh_android_1", "mesh_android_2",
|
|
93
|
+
"confirmed_ios", "confirmed_android",
|
|
94
|
+
],
|
|
95
|
+
"Cloudflare-specific": [
|
|
96
|
+
"cloudscraper",
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Flattened list for quick membership checks
|
|
101
|
+
_ALL_IDENTIFIERS: List[str] = [
|
|
102
|
+
ident
|
|
103
|
+
for group in SUPPORTED_CLIENT_IDENTIFIERS.values()
|
|
104
|
+
for ident in group
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def list_client_identifiers() -> List[str]:
|
|
109
|
+
"""Return a flat sorted list of all supported TLS client identifiers.
|
|
110
|
+
|
|
111
|
+
>>> from tls_client import list_client_identifiers
|
|
112
|
+
>>> "chrome_146" in list_client_identifiers()
|
|
113
|
+
True
|
|
114
|
+
"""
|
|
115
|
+
return sorted(_ALL_IDENTIFIERS)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# 强类型请求对象 ─ 提供 IDE 自动补全
|
|
120
|
+
# Strongly-typed request object — provides IDE autocompletion
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
class Request(TypedDict, total=False):
|
|
124
|
+
"""强类型请求对象,提供 IDE 自动补全。
|
|
125
|
+
|
|
126
|
+
Strongly-typed request object for IDE autocompletion.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
method: str
|
|
130
|
+
"""HTTP 方法,如 ``"GET"``、``"POST"``。在 :meth:`Session.typed_request` 中必填。
|
|
131
|
+
|
|
132
|
+
HTTP method, e.g. ``"GET"``, ``"POST"``. Required in :meth:`Session.typed_request`.
|
|
133
|
+
"""
|
|
134
|
+
url: str
|
|
135
|
+
"""请求 URL。在 :meth:`Session.typed_request` 中必填。
|
|
136
|
+
|
|
137
|
+
Request URL. Required in :meth:`Session.typed_request`.
|
|
138
|
+
"""
|
|
139
|
+
headers: Optional[Dict[str, str]]
|
|
140
|
+
"""请求头字典。
|
|
141
|
+
|
|
142
|
+
Request headers dict.
|
|
143
|
+
"""
|
|
144
|
+
header_order: Optional[List[str]]
|
|
145
|
+
"""请求头发送顺序列表。
|
|
146
|
+
|
|
147
|
+
Header send-order list.
|
|
148
|
+
"""
|
|
149
|
+
body: Optional[bytes]
|
|
150
|
+
"""请求体原始字节串。
|
|
151
|
+
|
|
152
|
+
Request body as raw bytes.
|
|
153
|
+
"""
|
|
154
|
+
client_identifier: Optional[str]
|
|
155
|
+
"""TLS 指纹标识。
|
|
156
|
+
|
|
157
|
+
TLS fingerprint identifier.
|
|
158
|
+
"""
|
|
159
|
+
timeout: Optional[int]
|
|
160
|
+
"""超时时间(秒)。
|
|
161
|
+
|
|
162
|
+
Timeout in seconds.
|
|
163
|
+
"""
|
|
164
|
+
timeout_milliseconds: Optional[int]
|
|
165
|
+
"""超时时间(毫秒)。
|
|
166
|
+
|
|
167
|
+
Timeout in milliseconds.
|
|
168
|
+
"""
|
|
169
|
+
follow_redirects: Optional[bool]
|
|
170
|
+
"""是否跟随重定向。
|
|
171
|
+
|
|
172
|
+
Whether to follow redirects.
|
|
173
|
+
"""
|
|
174
|
+
verify: Optional[bool]
|
|
175
|
+
"""是否验证 TLS 证书。
|
|
176
|
+
|
|
177
|
+
Whether to verify the TLS certificate.
|
|
178
|
+
"""
|
|
179
|
+
force_http1: Optional[bool]
|
|
180
|
+
"""是否强制 HTTP/1.1。
|
|
181
|
+
|
|
182
|
+
Whether to force HTTP/1.1.
|
|
183
|
+
"""
|
|
184
|
+
random_tls_extension_order: Optional[bool]
|
|
185
|
+
"""是否随机 TLS 扩展顺序。
|
|
186
|
+
|
|
187
|
+
Whether to randomise TLS extension order.
|
|
188
|
+
"""
|
|
189
|
+
with_protocol_racing: Optional[bool]
|
|
190
|
+
"""是否启用协议竞速。
|
|
191
|
+
|
|
192
|
+
Whether to enable protocol racing.
|
|
193
|
+
"""
|
|
194
|
+
server_name_overwrite: Optional[str]
|
|
195
|
+
"""SNI 主机名覆盖。
|
|
196
|
+
|
|
197
|
+
SNI hostname override.
|
|
198
|
+
"""
|
|
199
|
+
request_host_override: Optional[str]
|
|
200
|
+
"""Host 请求头覆盖。
|
|
201
|
+
|
|
202
|
+
Host header override.
|
|
203
|
+
"""
|
|
204
|
+
proxy: Optional[str]
|
|
205
|
+
"""代理 URL。
|
|
206
|
+
|
|
207
|
+
Proxy URL.
|
|
208
|
+
"""
|
|
209
|
+
local_address: Optional[str]
|
|
210
|
+
"""本地绑定地址。
|
|
211
|
+
|
|
212
|
+
Local bind address.
|
|
213
|
+
"""
|
|
214
|
+
pseudo_header_order: Optional[List[str]]
|
|
215
|
+
"""HTTP/2 伪头顺序。
|
|
216
|
+
|
|
217
|
+
HTTP/2 pseudo-header order.
|
|
218
|
+
"""
|
|
219
|
+
h3_pseudo_header_order: Optional[List[str]]
|
|
220
|
+
"""HTTP/3 伪头顺序。
|
|
221
|
+
|
|
222
|
+
HTTP/3 pseudo-header order.
|
|
223
|
+
"""
|
|
224
|
+
max_idle_connections: Optional[int]
|
|
225
|
+
"""全局最大空闲连接数。
|
|
226
|
+
|
|
227
|
+
Global max idle connections.
|
|
228
|
+
"""
|
|
229
|
+
max_idle_connections_per_host: Optional[int]
|
|
230
|
+
"""每 Host 最大空闲连接数。
|
|
231
|
+
|
|
232
|
+
Max idle connections per host.
|
|
233
|
+
"""
|
|
234
|
+
max_connections_per_host: Optional[int]
|
|
235
|
+
"""每 Host 最大总连接数。
|
|
236
|
+
|
|
237
|
+
Max total connections per host.
|
|
238
|
+
"""
|
|
239
|
+
disable_keep_alives: Optional[bool]
|
|
240
|
+
"""是否禁用 Keep-Alive。
|
|
241
|
+
|
|
242
|
+
Whether to disable Keep-Alive.
|
|
243
|
+
"""
|
|
244
|
+
disable_compression: Optional[bool]
|
|
245
|
+
"""是否禁用响应解压。
|
|
246
|
+
|
|
247
|
+
Whether to disable decompression.
|
|
248
|
+
"""
|
|
249
|
+
idle_conn_timeout_seconds: Optional[int]
|
|
250
|
+
"""空闲连接超时(秒)。
|
|
251
|
+
|
|
252
|
+
Idle connection timeout (seconds).
|
|
253
|
+
"""
|
|
254
|
+
max_response_header_bytes: Optional[int]
|
|
255
|
+
"""响应头最大字节数。
|
|
256
|
+
|
|
257
|
+
Max response header bytes.
|
|
258
|
+
"""
|
|
259
|
+
write_buffer_size: Optional[int]
|
|
260
|
+
"""写缓冲区大小。
|
|
261
|
+
|
|
262
|
+
Write buffer size.
|
|
263
|
+
"""
|
|
264
|
+
read_buffer_size: Optional[int]
|
|
265
|
+
"""读缓冲区大小。
|
|
266
|
+
|
|
267
|
+
Read buffer size.
|
|
268
|
+
"""
|
|
269
|
+
allow_empty_cookies: Optional[bool]
|
|
270
|
+
"""是否允许空 Cookie。
|
|
271
|
+
|
|
272
|
+
Whether to allow empty cookies.
|
|
273
|
+
"""
|
|
274
|
+
without_cookie_jar: Optional[bool]
|
|
275
|
+
"""是否禁用 Cookie Jar。
|
|
276
|
+
|
|
277
|
+
Whether to disable Cookie Jar.
|
|
278
|
+
"""
|
|
279
|
+
disable_http3: Optional[bool]
|
|
280
|
+
"""是否禁用 HTTP/3。
|
|
281
|
+
|
|
282
|
+
Whether to disable HTTP/3.
|
|
283
|
+
"""
|
|
284
|
+
disable_ipv4: Optional[bool]
|
|
285
|
+
"""是否禁用 IPv4。
|
|
286
|
+
|
|
287
|
+
Whether to disable IPv4.
|
|
288
|
+
"""
|
|
289
|
+
disable_ipv6: Optional[bool]
|
|
290
|
+
"""是否禁用 IPv6。
|
|
291
|
+
|
|
292
|
+
Whether to disable IPv6.
|
|
293
|
+
"""
|
|
294
|
+
catch_panics: Optional[bool]
|
|
295
|
+
"""是否捕获 Go panic。
|
|
296
|
+
|
|
297
|
+
Whether to catch Go panics.
|
|
298
|
+
"""
|
|
299
|
+
with_debug: Optional[bool]
|
|
300
|
+
"""是否启用调试日志。
|
|
301
|
+
|
|
302
|
+
Whether to enable debug logging.
|
|
303
|
+
"""
|
|
304
|
+
default_headers: Optional[Dict[str, str]]
|
|
305
|
+
"""默认请求头字典,每次请求自动附加。
|
|
306
|
+
|
|
307
|
+
Default headers dict, automatically added to every request.
|
|
308
|
+
"""
|
|
309
|
+
connect_headers: Optional[Dict[str, str]]
|
|
310
|
+
"""代理 CONNECT 隧道请求头字典。
|
|
311
|
+
|
|
312
|
+
Proxy CONNECT tunnel headers dict.
|
|
313
|
+
"""
|
|
314
|
+
certificate_pinning_hosts: Optional[Dict[str, List[str]]]
|
|
315
|
+
"""SSL 证书固定字典,格式为 ``{host: [pin1, pin2, ...]}``。
|
|
316
|
+
|
|
317
|
+
SSL certificate pinning dict, format ``{host: [pin1, pin2, ...]}``.
|
|
318
|
+
"""
|
|
319
|
+
with_default_bad_pin_handler: Optional[bool]
|
|
320
|
+
"""是否在证书固定失败时调用默认 Bad-Pin 处理器。
|
|
321
|
+
|
|
322
|
+
Whether to invoke the default bad-pin handler on pin mismatch.
|
|
323
|
+
"""
|
|
324
|
+
request_cookies: Optional[Dict[str, str]]
|
|
325
|
+
"""预置 Cookie 字典,在发送请求前注入到 Cookie Jar。
|
|
326
|
+
|
|
327
|
+
Pre-populated cookie dict, injected into the Cookie Jar before sending.
|
|
328
|
+
"""
|
|
329
|
+
custom_tls_client: Optional[Dict[str, Any]]
|
|
330
|
+
"""完全自定义的 TLS 客户端配置字典。
|
|
331
|
+
|
|
332
|
+
Fully custom TLS client configuration dict.
|
|
333
|
+
|
|
334
|
+
包含 JA3 字符串、密码套件、ECH、ALPN、HTTP/2/3 设置等 26 个字段。
|
|
335
|
+
设置后将忽略 ``client_identifier``。
|
|
336
|
+
|
|
337
|
+
Contains JA3 string, cipher suites, ECH, ALPN, HTTP/2/3 settings, etc.
|
|
338
|
+
(26 fields). When set, ``client_identifier`` is ignored.
|
|
339
|
+
"""
|
|
340
|
+
client_certificates: Optional[List[Dict[str, bytes]]]
|
|
341
|
+
"""客户端证书列表,用于 mTLS 双向认证。
|
|
342
|
+
|
|
343
|
+
每个元素为 ``{'cert_pem': bytes, 'key_pem': bytes}``。
|
|
344
|
+
|
|
345
|
+
Client certificate list for mTLS mutual authentication.
|
|
346
|
+
|
|
347
|
+
Each element is ``{'cert_pem': bytes, 'key_pem': bytes}``.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# CFFI cdef – must stay byte‑identical with main.go `import "C"` block
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
CDEF = """
|
|
356
|
+
typedef struct {
|
|
357
|
+
const char* key;
|
|
358
|
+
const char* value;
|
|
359
|
+
} HttpHeader;
|
|
360
|
+
|
|
361
|
+
typedef struct {
|
|
362
|
+
const char* host;
|
|
363
|
+
const char** pins;
|
|
364
|
+
int pins_len;
|
|
365
|
+
} PinEntry;
|
|
366
|
+
|
|
367
|
+
typedef struct {
|
|
368
|
+
unsigned int streamDep;
|
|
369
|
+
int exclusive;
|
|
370
|
+
unsigned char weight;
|
|
371
|
+
} PriorityParam;
|
|
372
|
+
|
|
373
|
+
typedef struct {
|
|
374
|
+
PriorityParam priorityParam;
|
|
375
|
+
unsigned int streamID;
|
|
376
|
+
} PriorityFrame;
|
|
377
|
+
|
|
378
|
+
typedef struct {
|
|
379
|
+
const char* kdfId;
|
|
380
|
+
const char* aeadId;
|
|
381
|
+
} CandidateCipherSuite;
|
|
382
|
+
|
|
383
|
+
typedef struct {
|
|
384
|
+
const char* cert_pem;
|
|
385
|
+
int cert_pem_len;
|
|
386
|
+
const char* key_pem;
|
|
387
|
+
int key_pem_len;
|
|
388
|
+
} ClientCertificate;
|
|
389
|
+
|
|
390
|
+
typedef struct {
|
|
391
|
+
const char** h2_settings_keys;
|
|
392
|
+
unsigned int* h2_settings_values;
|
|
393
|
+
int h2_settings_len;
|
|
394
|
+
const char** h2_settings_order;
|
|
395
|
+
int h2_settings_order_len;
|
|
396
|
+
const char** h3_settings_keys;
|
|
397
|
+
unsigned long long* h3_settings_values;
|
|
398
|
+
int h3_settings_len;
|
|
399
|
+
const char** h3_settings_order;
|
|
400
|
+
int h3_settings_order_len;
|
|
401
|
+
const char** h3_pseudo_header_order;
|
|
402
|
+
int h3_pseudo_header_order_len;
|
|
403
|
+
PriorityParam* header_priority;
|
|
404
|
+
const char** cert_compression_algos;
|
|
405
|
+
int cert_compression_algos_len;
|
|
406
|
+
const char* ja3_string;
|
|
407
|
+
const char** key_share_curves;
|
|
408
|
+
int key_share_curves_len;
|
|
409
|
+
const char** alpn_protocols;
|
|
410
|
+
int alpn_protocols_len;
|
|
411
|
+
const char** alps_protocols;
|
|
412
|
+
int alps_protocols_len;
|
|
413
|
+
unsigned short* ech_candidate_payloads;
|
|
414
|
+
int ech_candidate_payloads_len;
|
|
415
|
+
CandidateCipherSuite* ech_candidate_cipher_suites;
|
|
416
|
+
int ech_candidate_cipher_suites_len;
|
|
417
|
+
PriorityFrame* priority_frames;
|
|
418
|
+
int priority_frames_len;
|
|
419
|
+
const char** pseudo_header_order;
|
|
420
|
+
int pseudo_header_order_len;
|
|
421
|
+
const char** supported_delegated_credentials_algorithms;
|
|
422
|
+
int supported_delegated_credentials_algorithms_len;
|
|
423
|
+
const char** supported_signature_algorithms;
|
|
424
|
+
int supported_signature_algorithms_len;
|
|
425
|
+
const char** supported_versions;
|
|
426
|
+
int supported_versions_len;
|
|
427
|
+
unsigned int connection_flow;
|
|
428
|
+
unsigned short record_size_limit;
|
|
429
|
+
unsigned int stream_id;
|
|
430
|
+
unsigned int h3_priority_param;
|
|
431
|
+
int h3_send_grease_frames;
|
|
432
|
+
int allow_http;
|
|
433
|
+
} CustomTlsClient;
|
|
434
|
+
|
|
435
|
+
typedef struct {
|
|
436
|
+
const char* method;
|
|
437
|
+
const char* url;
|
|
438
|
+
const char* body;
|
|
439
|
+
int body_len;
|
|
440
|
+
const char* proxy;
|
|
441
|
+
const char* client_identifier;
|
|
442
|
+
HttpHeader* headers;
|
|
443
|
+
int headers_len;
|
|
444
|
+
const char** header_order;
|
|
445
|
+
int header_order_len;
|
|
446
|
+
const char** pseudo_header_order;
|
|
447
|
+
int pseudo_header_order_len;
|
|
448
|
+
const char** h3_pseudo_header_order;
|
|
449
|
+
int h3_pseudo_header_order_len;
|
|
450
|
+
int timeout_seconds;
|
|
451
|
+
int timeout_milliseconds;
|
|
452
|
+
int follow_redirects;
|
|
453
|
+
int insecure_skip_verify;
|
|
454
|
+
int force_http1;
|
|
455
|
+
int with_random_tls_extension_order;
|
|
456
|
+
int with_protocol_racing;
|
|
457
|
+
const char* server_name_overwrite;
|
|
458
|
+
const char* request_host_override;
|
|
459
|
+
const char* local_address;
|
|
460
|
+
int max_idle_connections;
|
|
461
|
+
int max_idle_connections_per_host;
|
|
462
|
+
int max_connections_per_host;
|
|
463
|
+
int max_response_header_bytes;
|
|
464
|
+
int write_buffer_size;
|
|
465
|
+
int read_buffer_size;
|
|
466
|
+
int idle_conn_timeout_seconds;
|
|
467
|
+
int disable_keep_alives;
|
|
468
|
+
int disable_compression;
|
|
469
|
+
int allow_empty_cookies;
|
|
470
|
+
int disable_http3;
|
|
471
|
+
int disable_ipv4;
|
|
472
|
+
int disable_ipv6;
|
|
473
|
+
int without_cookie_jar;
|
|
474
|
+
int catch_panics;
|
|
475
|
+
int with_debug;
|
|
476
|
+
const char* stream_output_path;
|
|
477
|
+
int stream_output_block_size;
|
|
478
|
+
const char* stream_output_eof_symbol;
|
|
479
|
+
HttpHeader* default_headers;
|
|
480
|
+
int default_headers_len;
|
|
481
|
+
HttpHeader* connect_headers;
|
|
482
|
+
int connect_headers_len;
|
|
483
|
+
PinEntry* certificate_pinning_hosts;
|
|
484
|
+
int certificate_pinning_hosts_len;
|
|
485
|
+
int with_default_bad_pin_handler;
|
|
486
|
+
HttpHeader* request_cookies;
|
|
487
|
+
int request_cookies_len;
|
|
488
|
+
ClientCertificate* client_certificates;
|
|
489
|
+
int client_certificates_len;
|
|
490
|
+
CustomTlsClient* custom_tls_client;
|
|
491
|
+
} RequestOptions;
|
|
492
|
+
|
|
493
|
+
typedef struct {
|
|
494
|
+
int status_code;
|
|
495
|
+
char* body;
|
|
496
|
+
int body_len;
|
|
497
|
+
char* err_msg;
|
|
498
|
+
char* target_url;
|
|
499
|
+
char* used_protocol;
|
|
500
|
+
HttpHeader* response_headers;
|
|
501
|
+
int response_headers_len;
|
|
502
|
+
HttpHeader* cookies;
|
|
503
|
+
int cookies_len;
|
|
504
|
+
} ResponseResult;
|
|
505
|
+
|
|
506
|
+
ResponseResult* ExecuteRequest(RequestOptions* opts);
|
|
507
|
+
void FreeResponse(ResponseResult* res);
|
|
508
|
+
void ClearClientPool(void);
|
|
509
|
+
|
|
510
|
+
typedef void (*async_callback_fn)(uintptr_t request_id, ResponseResult* response);
|
|
511
|
+
int RequestAsync(RequestOptions* opts, uintptr_t request_id, async_callback_fn cb);
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
# Platform detection & shared‑library loading
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
def _detect_libc() -> str:
|
|
519
|
+
"""Detect the C library flavour on Linux. Returns ``"glibc"``, ``"musl"``, or ``"unknown"``.
|
|
520
|
+
|
|
521
|
+
This is important because a shared library compiled against glibc
|
|
522
|
+
will not load on a musl-based distribution (Alpine, Void musl, etc.)
|
|
523
|
+
and vice versa.
|
|
524
|
+
"""
|
|
525
|
+
if sys.platform != "linux":
|
|
526
|
+
return "glibc" # non‑Linux → irrelevant
|
|
527
|
+
try:
|
|
528
|
+
# musl defines a weak symbol __musl__ that glibc does not.
|
|
529
|
+
libc = ctypes.CDLL(None) # the process itself
|
|
530
|
+
try:
|
|
531
|
+
libc.__musl__ # pylint: disable=pointless-statement
|
|
532
|
+
return "musl"
|
|
533
|
+
except AttributeError:
|
|
534
|
+
return "glibc"
|
|
535
|
+
except Exception:
|
|
536
|
+
# Fallback: check for /lib/ld-musl-*.so*
|
|
537
|
+
for entry in Path("/lib").glob("ld-musl-*"):
|
|
538
|
+
if entry.is_file():
|
|
539
|
+
return "musl"
|
|
540
|
+
return "unknown"
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _libc_suffix() -> str:
|
|
544
|
+
"""Return an optional libc suffix for the library filename.
|
|
545
|
+
|
|
546
|
+
Alpine (musl) binaries carry a ``-musl`` suffix so they don't collide
|
|
547
|
+
with glibc builds on PyPI.
|
|
548
|
+
"""
|
|
549
|
+
if sys.platform == "linux" and _detect_libc() == "musl":
|
|
550
|
+
return "-musl"
|
|
551
|
+
return ""
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _go_os() -> str:
|
|
555
|
+
return {"darwin": "darwin", "linux": "linux", "win32": "windows"}.get(
|
|
556
|
+
sys.platform, sys.platform
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _go_arch() -> str:
|
|
561
|
+
m = platform.machine().lower()
|
|
562
|
+
return {
|
|
563
|
+
"x86_64": "amd64", "amd64": "amd64",
|
|
564
|
+
"arm64": "arm64", "aarch64": "arm64",
|
|
565
|
+
"armv7l": "arm", "armv6l": "arm",
|
|
566
|
+
"i386": "386", "i686": "386",
|
|
567
|
+
}.get(m, m)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _shared_lib_ext() -> str:
|
|
571
|
+
if sys.platform == "darwin":
|
|
572
|
+
return ".dylib"
|
|
573
|
+
elif sys.platform == "win32":
|
|
574
|
+
return ".dll"
|
|
575
|
+
return ".so"
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _shared_lib_name() -> str:
|
|
579
|
+
ext = _shared_lib_ext()
|
|
580
|
+
goos = _go_os()
|
|
581
|
+
goarch = _go_arch()
|
|
582
|
+
lc = _libc_suffix()
|
|
583
|
+
return f"tls-client-{goos}-{goarch}{lc}{ext}"
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _find_library() -> str:
|
|
587
|
+
"""Locate the platform‑appropriate shared library.
|
|
588
|
+
|
|
589
|
+
Search order:
|
|
590
|
+
1. ``TLS_CLIENT_LIB`` environment variable (explicit user override).
|
|
591
|
+
2. Bundled binary in ``tls_client/bin/`` (the package directory).
|
|
592
|
+
3. System‑level ``dist/`` directory next to this module (dev convenience).
|
|
593
|
+
|
|
594
|
+
Raises :exc:`FileNotFoundError` with a detailed diagnostic message if
|
|
595
|
+
no binary is found, including hints for Alpine/musl users.
|
|
596
|
+
"""
|
|
597
|
+
# 1. Explicit override
|
|
598
|
+
env_lib = os.environ.get("TLS_CLIENT_LIB")
|
|
599
|
+
if env_lib:
|
|
600
|
+
p = Path(env_lib)
|
|
601
|
+
if p.exists():
|
|
602
|
+
return str(p)
|
|
603
|
+
raise FileNotFoundError(
|
|
604
|
+
f"TLS_CLIENT_LIB is set to '{env_lib}' but the file does not exist."
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
name = _shared_lib_name()
|
|
608
|
+
here = Path(__file__).resolve().parent
|
|
609
|
+
|
|
610
|
+
# 2. Bundled binary in the package
|
|
611
|
+
bundled = here / "bin" / name
|
|
612
|
+
if bundled.exists():
|
|
613
|
+
return str(bundled)
|
|
614
|
+
|
|
615
|
+
# 3. Development convenience – dist/ directory next to this module
|
|
616
|
+
dev = here / "dist" / name
|
|
617
|
+
if dev.exists():
|
|
618
|
+
return str(dev)
|
|
619
|
+
|
|
620
|
+
# ---- diagnostic error message ----
|
|
621
|
+
parts = [
|
|
622
|
+
f"Cannot locate shared library '{name}'.",
|
|
623
|
+
"",
|
|
624
|
+
f" Searched: {bundled}",
|
|
625
|
+
f" {dev}",
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
if sys.platform == "linux":
|
|
629
|
+
libc = _detect_libc()
|
|
630
|
+
parts.append(f" Detected libc: {libc}")
|
|
631
|
+
if libc == "musl":
|
|
632
|
+
parts.append(
|
|
633
|
+
" This is an Alpine / musl-based system. The pre-compiled glibc"
|
|
634
|
+
)
|
|
635
|
+
parts.append(
|
|
636
|
+
" binary cannot be used. Build a musl-compatible library via:"
|
|
637
|
+
)
|
|
638
|
+
parts.append(
|
|
639
|
+
" cd cffi_binding && CGO_ENABLED=1 GOOS=linux GOARCH="
|
|
640
|
+
+ _go_arch()
|
|
641
|
+
+ " go build -buildmode=c-shared -o tls-client-linux-"
|
|
642
|
+
+ _go_arch()
|
|
643
|
+
+ "-musl.so ."
|
|
644
|
+
)
|
|
645
|
+
else:
|
|
646
|
+
parts.append(
|
|
647
|
+
" Make sure the platform matches. Expected: "
|
|
648
|
+
+ _go_os()
|
|
649
|
+
+ "/"
|
|
650
|
+
+ _go_arch()
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
parts.append(
|
|
654
|
+
" Set the TLS_CLIENT_LIB environment variable to point to the correct binary."
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
raise FileNotFoundError("\n".join(parts))
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _load_ffi():
|
|
661
|
+
"""Lazy‑import cffi so the wrapper is importable without it installed."""
|
|
662
|
+
from cffi import FFI
|
|
663
|
+
|
|
664
|
+
ffi = FFI()
|
|
665
|
+
ffi.cdef(CDEF)
|
|
666
|
+
libpath = _find_library()
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
lib = ffi.dlopen(libpath)
|
|
670
|
+
except OSError as exc:
|
|
671
|
+
raise OSError(
|
|
672
|
+
f"Shared library at '{libpath}' failed to load. "
|
|
673
|
+
f"The binary may be corrupt, built for a different architecture, "
|
|
674
|
+
f"or missing system dependencies.\n"
|
|
675
|
+
f"Underlying error: {exc}"
|
|
676
|
+
) from exc
|
|
677
|
+
|
|
678
|
+
return ffi, lib
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# Global singletons – initialised on first use
|
|
682
|
+
_ffi = None
|
|
683
|
+
_lib = None
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _get_ffi():
|
|
687
|
+
global _ffi, _lib
|
|
688
|
+
if _ffi is None:
|
|
689
|
+
_ffi, _lib = _load_ffi()
|
|
690
|
+
return _ffi, _lib
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
# ---------------------------------------------------------------------------
|
|
694
|
+
# Helpers
|
|
695
|
+
# ---------------------------------------------------------------------------
|
|
696
|
+
|
|
697
|
+
def _charset_from_content_type(content_type: str) -> str:
|
|
698
|
+
"""Extract charset from Content-Type header, default to ``"utf-8"``.
|
|
699
|
+
|
|
700
|
+
Mirrors ``golang.org/x/net/html/charset.NewReader`` behaviour.
|
|
701
|
+
"""
|
|
702
|
+
if not content_type:
|
|
703
|
+
return "utf-8"
|
|
704
|
+
# Parse 'text/html; charset=gbk' → 'gbk'
|
|
705
|
+
for part in content_type.lower().split(";"):
|
|
706
|
+
part = part.strip()
|
|
707
|
+
if part.startswith("charset=") or part.startswith("charset ="):
|
|
708
|
+
enc = part.split("=", 1)[1].strip().strip('"').strip("'")
|
|
709
|
+
if enc:
|
|
710
|
+
return enc
|
|
711
|
+
return "utf-8"
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _c_string(ffi, value: Optional[str]) -> Any:
|
|
715
|
+
"""Allocate a null‑terminated C string (or ``ffi.NULL``)."""
|
|
716
|
+
if value is None:
|
|
717
|
+
return ffi.NULL
|
|
718
|
+
return ffi.new("char[]", value.encode("utf-8"))
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _build_headers(ffi, headers: Optional[Dict[str, str]], keep_alive: list):
|
|
722
|
+
"""Convert a Python dict → C HttpHeader[]. Returns ``(ptr, length)``."""
|
|
723
|
+
if not headers:
|
|
724
|
+
return ffi.NULL, 0
|
|
725
|
+
|
|
726
|
+
n = len(headers)
|
|
727
|
+
arr = ffi.new("HttpHeader[]", n)
|
|
728
|
+
keep_alive.append(arr)
|
|
729
|
+
|
|
730
|
+
for i, (k, v) in enumerate(headers.items()):
|
|
731
|
+
ck = ffi.new("char[]", k.encode("utf-8"))
|
|
732
|
+
cv = ffi.new("char[]", v.encode("utf-8"))
|
|
733
|
+
keep_alive.extend([ck, cv])
|
|
734
|
+
arr[i].key = ck
|
|
735
|
+
arr[i].value = cv
|
|
736
|
+
|
|
737
|
+
return arr, n
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _build_string_array(ffi, items: Optional[List[str]], keep_alive: list):
|
|
741
|
+
"""Convert a Python list[str] → C const char**. Returns ``(ptr, length)``."""
|
|
742
|
+
if not items:
|
|
743
|
+
return ffi.NULL, 0
|
|
744
|
+
|
|
745
|
+
n = len(items)
|
|
746
|
+
arr = ffi.new("const char*[]", n)
|
|
747
|
+
keep_alive.append(arr)
|
|
748
|
+
|
|
749
|
+
for i, s in enumerate(items):
|
|
750
|
+
cs = ffi.new("char[]", s.encode("utf-8"))
|
|
751
|
+
keep_alive.append(cs)
|
|
752
|
+
arr[i] = cs
|
|
753
|
+
|
|
754
|
+
return arr, n
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _build_pin_entries(ffi, pins: Optional[Dict[str, List[str]]], keep_alive: list):
|
|
758
|
+
"""Convert a Python dict[str, list[str]] → C PinEntry[]. Returns ``(ptr, length)``."""
|
|
759
|
+
if not pins:
|
|
760
|
+
return ffi.NULL, 0
|
|
761
|
+
|
|
762
|
+
n = len(pins)
|
|
763
|
+
arr = ffi.new("PinEntry[]", n)
|
|
764
|
+
keep_alive.append(arr)
|
|
765
|
+
|
|
766
|
+
for i, (host, pin_list) in enumerate(pins.items()):
|
|
767
|
+
ch = ffi.new("char[]", host.encode("utf-8"))
|
|
768
|
+
keep_alive.append(ch)
|
|
769
|
+
arr[i].host = ch
|
|
770
|
+
|
|
771
|
+
if pin_list:
|
|
772
|
+
pn = len(pin_list)
|
|
773
|
+
parr = ffi.new("const char*[]", pn)
|
|
774
|
+
keep_alive.append(parr)
|
|
775
|
+
for j, pin in enumerate(pin_list):
|
|
776
|
+
cp = ffi.new("char[]", pin.encode("utf-8"))
|
|
777
|
+
keep_alive.append(cp)
|
|
778
|
+
parr[j] = cp
|
|
779
|
+
arr[i].pins = parr
|
|
780
|
+
arr[i].pins_len = pn
|
|
781
|
+
else:
|
|
782
|
+
arr[i].pins = ffi.NULL
|
|
783
|
+
arr[i].pins_len = 0
|
|
784
|
+
|
|
785
|
+
return arr, n
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _build_client_certificates(ffi, certs: Optional[List[Dict[str, bytes]]], keep_alive: list):
|
|
789
|
+
"""Convert a Python list[dict] → C ClientCertificate[]. Returns ``(ptr, length)``.
|
|
790
|
+
|
|
791
|
+
Each dict must have ``"cert_pem"`` and ``"key_pem"`` keys with bytes values.
|
|
792
|
+
"""
|
|
793
|
+
if not certs:
|
|
794
|
+
return ffi.NULL, 0
|
|
795
|
+
|
|
796
|
+
n = len(certs)
|
|
797
|
+
arr = ffi.new("ClientCertificate[]", n)
|
|
798
|
+
keep_alive.append(arr)
|
|
799
|
+
|
|
800
|
+
for i, c in enumerate(certs):
|
|
801
|
+
cert_bytes = c.get("cert_pem", b"")
|
|
802
|
+
key_bytes = c.get("key_pem", b"")
|
|
803
|
+
|
|
804
|
+
cc = ffi.new("char[]", cert_bytes)
|
|
805
|
+
ck = ffi.new("char[]", key_bytes)
|
|
806
|
+
keep_alive.extend([cc, ck])
|
|
807
|
+
arr[i].cert_pem = cc
|
|
808
|
+
arr[i].cert_pem_len = len(cert_bytes)
|
|
809
|
+
arr[i].key_pem = ck
|
|
810
|
+
arr[i].key_pem_len = len(key_bytes)
|
|
811
|
+
|
|
812
|
+
return arr, n
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _build_custom_tls_client(ffi, cfg: Optional[Dict[str, Any]], keep_alive: list):
|
|
816
|
+
"""Convert a Python dict → C CustomTlsClient*. Returns ``ffi.NULL`` when
|
|
817
|
+
*cfg* is None or empty."""
|
|
818
|
+
if not cfg:
|
|
819
|
+
return ffi.NULL
|
|
820
|
+
|
|
821
|
+
ctc = ffi.new("CustomTlsClient *")
|
|
822
|
+
keep_alive.append(ctc)
|
|
823
|
+
|
|
824
|
+
# JA3 string
|
|
825
|
+
ja3 = _c_string(ffi, cfg.get("ja3_string"))
|
|
826
|
+
if ja3 != ffi.NULL:
|
|
827
|
+
keep_alive.append(ja3)
|
|
828
|
+
ctc.ja3_string = ja3
|
|
829
|
+
|
|
830
|
+
# Simple scalar fields
|
|
831
|
+
ctc.connection_flow = cfg.get("connection_flow", 0)
|
|
832
|
+
ctc.record_size_limit = cfg.get("record_size_limit", 0)
|
|
833
|
+
ctc.stream_id = cfg.get("stream_id", 0)
|
|
834
|
+
ctc.h3_priority_param = cfg.get("h3_priority_param", 0)
|
|
835
|
+
ctc.h3_send_grease_frames = 1 if cfg.get("h3_send_grease_frames") else 0
|
|
836
|
+
ctc.allow_http = 1 if cfg.get("allow_http") else 0
|
|
837
|
+
|
|
838
|
+
# String arrays
|
|
839
|
+
ctc.h2_settings_order, ctc.h2_settings_order_len = _build_string_array(
|
|
840
|
+
ffi, cfg.get("h2_settings_order"), keep_alive
|
|
841
|
+
)
|
|
842
|
+
ctc.h3_settings_order, ctc.h3_settings_order_len = _build_string_array(
|
|
843
|
+
ffi, cfg.get("h3_settings_order"), keep_alive
|
|
844
|
+
)
|
|
845
|
+
ctc.h3_pseudo_header_order, ctc.h3_pseudo_header_order_len = _build_string_array(
|
|
846
|
+
ffi, cfg.get("h3_pseudo_header_order"), keep_alive
|
|
847
|
+
)
|
|
848
|
+
ctc.cert_compression_algos, ctc.cert_compression_algos_len = _build_string_array(
|
|
849
|
+
ffi, cfg.get("cert_compression_algos"), keep_alive
|
|
850
|
+
)
|
|
851
|
+
ctc.key_share_curves, ctc.key_share_curves_len = _build_string_array(
|
|
852
|
+
ffi, cfg.get("key_share_curves"), keep_alive
|
|
853
|
+
)
|
|
854
|
+
ctc.alpn_protocols, ctc.alpn_protocols_len = _build_string_array(
|
|
855
|
+
ffi, cfg.get("alpn_protocols"), keep_alive
|
|
856
|
+
)
|
|
857
|
+
ctc.alps_protocols, ctc.alps_protocols_len = _build_string_array(
|
|
858
|
+
ffi, cfg.get("alps_protocols"), keep_alive
|
|
859
|
+
)
|
|
860
|
+
ctc.pseudo_header_order, ctc.pseudo_header_order_len = _build_string_array(
|
|
861
|
+
ffi, cfg.get("pseudo_header_order"), keep_alive
|
|
862
|
+
)
|
|
863
|
+
ctc.supported_delegated_credentials_algorithms, ctc.supported_delegated_credentials_algorithms_len = _build_string_array(
|
|
864
|
+
ffi, cfg.get("supported_delegated_credentials_algorithms"), keep_alive
|
|
865
|
+
)
|
|
866
|
+
ctc.supported_signature_algorithms, ctc.supported_signature_algorithms_len = _build_string_array(
|
|
867
|
+
ffi, cfg.get("supported_signature_algorithms"), keep_alive
|
|
868
|
+
)
|
|
869
|
+
ctc.supported_versions, ctc.supported_versions_len = _build_string_array(
|
|
870
|
+
ffi, cfg.get("supported_versions"), keep_alive
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
# H2/H3 settings key-value pairs
|
|
874
|
+
h2_settings = cfg.get("h2_settings")
|
|
875
|
+
if h2_settings:
|
|
876
|
+
n = len(h2_settings)
|
|
877
|
+
keys = ffi.new("const char*[]", n)
|
|
878
|
+
vals = ffi.new("unsigned int[]", n)
|
|
879
|
+
keep_alive.extend([keys, vals])
|
|
880
|
+
for i, (k, v) in enumerate(h2_settings.items()):
|
|
881
|
+
ck = ffi.new("char[]", k.encode("utf-8"))
|
|
882
|
+
keep_alive.append(ck)
|
|
883
|
+
keys[i] = ck
|
|
884
|
+
vals[i] = v
|
|
885
|
+
ctc.h2_settings_keys = keys
|
|
886
|
+
ctc.h2_settings_values = vals
|
|
887
|
+
ctc.h2_settings_len = n
|
|
888
|
+
|
|
889
|
+
h3_settings = cfg.get("h3_settings")
|
|
890
|
+
if h3_settings:
|
|
891
|
+
n = len(h3_settings)
|
|
892
|
+
keys = ffi.new("const char*[]", n)
|
|
893
|
+
vals = ffi.new("unsigned long long[]", n)
|
|
894
|
+
keep_alive.extend([keys, vals])
|
|
895
|
+
for i, (k, v) in enumerate(h3_settings.items()):
|
|
896
|
+
ck = ffi.new("char[]", k.encode("utf-8"))
|
|
897
|
+
keep_alive.append(ck)
|
|
898
|
+
keys[i] = ck
|
|
899
|
+
vals[i] = v
|
|
900
|
+
ctc.h3_settings_keys = keys
|
|
901
|
+
ctc.h3_settings_values = vals
|
|
902
|
+
ctc.h3_settings_len = n
|
|
903
|
+
|
|
904
|
+
# ECH candidate payloads
|
|
905
|
+
ech_payloads = cfg.get("ech_candidate_payloads")
|
|
906
|
+
if ech_payloads:
|
|
907
|
+
n = len(ech_payloads)
|
|
908
|
+
arr = ffi.new("unsigned short[]", ech_payloads)
|
|
909
|
+
keep_alive.append(arr)
|
|
910
|
+
ctc.ech_candidate_payloads = arr
|
|
911
|
+
ctc.ech_candidate_payloads_len = n
|
|
912
|
+
|
|
913
|
+
# ECH candidate cipher suites
|
|
914
|
+
ech_suites = cfg.get("ech_candidate_cipher_suites")
|
|
915
|
+
if ech_suites:
|
|
916
|
+
n = len(ech_suites)
|
|
917
|
+
arr = ffi.new("CandidateCipherSuite[]", n)
|
|
918
|
+
keep_alive.append(arr)
|
|
919
|
+
for i, suite in enumerate(ech_suites):
|
|
920
|
+
kdf = _c_string(ffi, suite.get("kdfId"))
|
|
921
|
+
aead = _c_string(ffi, suite.get("aeadId"))
|
|
922
|
+
if kdf != ffi.NULL:
|
|
923
|
+
keep_alive.append(kdf)
|
|
924
|
+
if aead != ffi.NULL:
|
|
925
|
+
keep_alive.append(aead)
|
|
926
|
+
arr[i].kdfId = kdf
|
|
927
|
+
arr[i].aeadId = aead
|
|
928
|
+
ctc.ech_candidate_cipher_suites = arr
|
|
929
|
+
ctc.ech_candidate_cipher_suites_len = n
|
|
930
|
+
|
|
931
|
+
# Priority frames
|
|
932
|
+
pri_frames = cfg.get("priority_frames")
|
|
933
|
+
if pri_frames:
|
|
934
|
+
n = len(pri_frames)
|
|
935
|
+
arr = ffi.new("PriorityFrame[]", n)
|
|
936
|
+
keep_alive.append(arr)
|
|
937
|
+
for i, pf in enumerate(pri_frames):
|
|
938
|
+
pp = pf.get("priorityParam", {})
|
|
939
|
+
arr[i].priorityParam.streamDep = pp.get("streamDep", 0)
|
|
940
|
+
arr[i].priorityParam.exclusive = 1 if pp.get("exclusive") else 0
|
|
941
|
+
arr[i].priorityParam.weight = pp.get("weight", 0)
|
|
942
|
+
arr[i].streamID = pf.get("streamID", 0)
|
|
943
|
+
ctc.priority_frames = arr
|
|
944
|
+
ctc.priority_frames_len = n
|
|
945
|
+
|
|
946
|
+
# Header priority (single optional struct)
|
|
947
|
+
hp = cfg.get("header_priority")
|
|
948
|
+
if hp:
|
|
949
|
+
chp = ffi.new("PriorityParam *")
|
|
950
|
+
keep_alive.append(chp)
|
|
951
|
+
chp.streamDep = hp.get("streamDep", 0)
|
|
952
|
+
chp.exclusive = 1 if hp.get("exclusive") else 0
|
|
953
|
+
chp.weight = hp.get("weight", 0)
|
|
954
|
+
ctc.header_priority = chp
|
|
955
|
+
|
|
956
|
+
return ctc
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
# ---------------------------------------------------------------------------
|
|
960
|
+
# Response unpacking — shared between sync ExecuteRequest and async callback
|
|
961
|
+
# ---------------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
def _unpack_c_response(ffi, raw_res) -> Response:
|
|
964
|
+
"""Unpack a C ResponseResult* into a Python Response object.
|
|
965
|
+
|
|
966
|
+
Used by both the synchronous execute_request path and the async
|
|
967
|
+
CFFI callback path. Does NOT call FreeResponse — the caller is
|
|
968
|
+
responsible for that.
|
|
969
|
+
"""
|
|
970
|
+
status = raw_res.status_code
|
|
971
|
+
blen = raw_res.body_len
|
|
972
|
+
|
|
973
|
+
if blen > 0 and raw_res.body != ffi.NULL:
|
|
974
|
+
body_bytes = ffi.buffer(raw_res.body, blen)[:]
|
|
975
|
+
else:
|
|
976
|
+
body_bytes = b""
|
|
977
|
+
|
|
978
|
+
response_headers: Dict[str, List[str]] = {}
|
|
979
|
+
rh_len = raw_res.response_headers_len
|
|
980
|
+
if rh_len > 0 and raw_res.response_headers != ffi.NULL:
|
|
981
|
+
for i in range(rh_len):
|
|
982
|
+
h = raw_res.response_headers[i]
|
|
983
|
+
if h.key != ffi.NULL and h.value != ffi.NULL:
|
|
984
|
+
k = ffi.string(h.key).decode("utf-8", errors="replace")
|
|
985
|
+
v = ffi.string(h.value).decode("utf-8", errors="replace")
|
|
986
|
+
response_headers.setdefault(k, []).append(v)
|
|
987
|
+
|
|
988
|
+
ct = ""
|
|
989
|
+
for k, v in response_headers.items():
|
|
990
|
+
if k.lower() == "content-type":
|
|
991
|
+
ct = v[0] if v else ""
|
|
992
|
+
break
|
|
993
|
+
encoding = _charset_from_content_type(ct)
|
|
994
|
+
try:
|
|
995
|
+
text = body_bytes.decode(encoding)
|
|
996
|
+
except (UnicodeDecodeError, LookupError):
|
|
997
|
+
encoding = "utf-8"
|
|
998
|
+
text = body_bytes.decode("utf-8", errors="replace")
|
|
999
|
+
|
|
1000
|
+
response_cookies: Dict[str, str] = {}
|
|
1001
|
+
ck_len = raw_res.cookies_len
|
|
1002
|
+
if ck_len > 0 and raw_res.cookies != ffi.NULL:
|
|
1003
|
+
for i in range(ck_len):
|
|
1004
|
+
ck_entry = raw_res.cookies[i]
|
|
1005
|
+
if ck_entry.key != ffi.NULL and ck_entry.value != ffi.NULL:
|
|
1006
|
+
cn = ffi.string(ck_entry.key).decode("utf-8", errors="replace")
|
|
1007
|
+
cv = ffi.string(ck_entry.value).decode("utf-8", errors="replace")
|
|
1008
|
+
response_cookies[cn] = cv
|
|
1009
|
+
|
|
1010
|
+
target_url: Optional[str] = None
|
|
1011
|
+
if raw_res.target_url != ffi.NULL:
|
|
1012
|
+
target_url = ffi.string(raw_res.target_url).decode("utf-8", errors="replace")
|
|
1013
|
+
|
|
1014
|
+
used_protocol: Optional[str] = None
|
|
1015
|
+
if raw_res.used_protocol != ffi.NULL:
|
|
1016
|
+
used_protocol = ffi.string(raw_res.used_protocol).decode("utf-8", errors="replace")
|
|
1017
|
+
|
|
1018
|
+
return Response(
|
|
1019
|
+
status_code=status,
|
|
1020
|
+
headers=response_headers,
|
|
1021
|
+
content=body_bytes,
|
|
1022
|
+
text=text,
|
|
1023
|
+
encoding=encoding,
|
|
1024
|
+
url=target_url,
|
|
1025
|
+
cookies=response_cookies,
|
|
1026
|
+
used_protocol=used_protocol,
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
# ---------------------------------------------------------------------------
|
|
1031
|
+
# Response – requests‑style response object
|
|
1032
|
+
# ---------------------------------------------------------------------------
|
|
1033
|
+
|
|
1034
|
+
class Response:
|
|
1035
|
+
"""HTTP 响应对象,兼容 requests 库风格。
|
|
1036
|
+
|
|
1037
|
+
A requests‑compatible HTTP response object.
|
|
1038
|
+
|
|
1039
|
+
Attributes
|
|
1040
|
+
----------
|
|
1041
|
+
status_code : int
|
|
1042
|
+
HTTP 状态码 / HTTP status code.
|
|
1043
|
+
headers : dict
|
|
1044
|
+
响应头字典 / Response headers.
|
|
1045
|
+
content : bytes
|
|
1046
|
+
原始响应体字节串 / Raw response body as bytes.
|
|
1047
|
+
text : str
|
|
1048
|
+
已解码的响应体文本 / Decoded response body text.
|
|
1049
|
+
encoding : str
|
|
1050
|
+
检测到的字符编码 / Detected charset encoding.
|
|
1051
|
+
url : str or None
|
|
1052
|
+
最终请求 URL(重定向后)/ Final URL after redirects.
|
|
1053
|
+
cookies : dict
|
|
1054
|
+
响应 Cookie 字典 / Response cookies dict.
|
|
1055
|
+
used_protocol : str or None
|
|
1056
|
+
实际使用的 HTTP 协议版本 / HTTP protocol version used.
|
|
1057
|
+
"""
|
|
1058
|
+
|
|
1059
|
+
__slots__ = (
|
|
1060
|
+
"_status_code",
|
|
1061
|
+
"_headers",
|
|
1062
|
+
"_content",
|
|
1063
|
+
"_text",
|
|
1064
|
+
"_encoding",
|
|
1065
|
+
"_url",
|
|
1066
|
+
"_cookies",
|
|
1067
|
+
"_used_protocol",
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
def __init__(
|
|
1071
|
+
self,
|
|
1072
|
+
status_code: int,
|
|
1073
|
+
headers: Dict[str, List[str]],
|
|
1074
|
+
content: bytes,
|
|
1075
|
+
text: str,
|
|
1076
|
+
encoding: str,
|
|
1077
|
+
url: Optional[str] = None,
|
|
1078
|
+
cookies: Optional[Dict[str, str]] = None,
|
|
1079
|
+
used_protocol: Optional[str] = None,
|
|
1080
|
+
) -> None:
|
|
1081
|
+
self._status_code = status_code
|
|
1082
|
+
self._headers = headers
|
|
1083
|
+
self._content = content
|
|
1084
|
+
self._text = text
|
|
1085
|
+
self._encoding = encoding
|
|
1086
|
+
self._url = url
|
|
1087
|
+
self._cookies = cookies or {}
|
|
1088
|
+
self._used_protocol = used_protocol
|
|
1089
|
+
|
|
1090
|
+
# -- read‑only properties ----------------------------------------------
|
|
1091
|
+
|
|
1092
|
+
@property
|
|
1093
|
+
def status_code(self) -> int:
|
|
1094
|
+
"""HTTP 状态码 / HTTP status code."""
|
|
1095
|
+
return self._status_code
|
|
1096
|
+
|
|
1097
|
+
@property
|
|
1098
|
+
def headers(self) -> Dict[str, List[str]]:
|
|
1099
|
+
"""响应头字典 / Response headers."""
|
|
1100
|
+
return self._headers
|
|
1101
|
+
|
|
1102
|
+
@property
|
|
1103
|
+
def content(self) -> bytes:
|
|
1104
|
+
"""原始响应体字节串 / Raw bytes body."""
|
|
1105
|
+
return self._content
|
|
1106
|
+
|
|
1107
|
+
@property
|
|
1108
|
+
def text(self) -> str:
|
|
1109
|
+
"""已解码的响应体文本 / Decoded text body."""
|
|
1110
|
+
return self._text
|
|
1111
|
+
|
|
1112
|
+
@property
|
|
1113
|
+
def encoding(self) -> str:
|
|
1114
|
+
"""文本编码 / Text encoding."""
|
|
1115
|
+
return self._encoding
|
|
1116
|
+
|
|
1117
|
+
@property
|
|
1118
|
+
def url(self) -> Optional[str]:
|
|
1119
|
+
"""最终请求 URL(重定向后)/ Final URL after redirects."""
|
|
1120
|
+
return self._url
|
|
1121
|
+
|
|
1122
|
+
@property
|
|
1123
|
+
def cookies(self) -> Dict[str, str]:
|
|
1124
|
+
"""响应 Cookie 字典 / Response cookies dict."""
|
|
1125
|
+
return self._cookies
|
|
1126
|
+
|
|
1127
|
+
@property
|
|
1128
|
+
def used_protocol(self) -> Optional[str]:
|
|
1129
|
+
"""使用的 HTTP 协议版本 / HTTP protocol version (e.g. ``"HTTP/2.0"``)."""
|
|
1130
|
+
return self._used_protocol
|
|
1131
|
+
|
|
1132
|
+
@property
|
|
1133
|
+
def ok(self) -> bool:
|
|
1134
|
+
"""状态码 < 400 时为 ``True``。 / ``True`` if status_code < 400."""
|
|
1135
|
+
return self._status_code < 400
|
|
1136
|
+
|
|
1137
|
+
@property
|
|
1138
|
+
def reason(self) -> str:
|
|
1139
|
+
"""HTTP 状态文本 / HTTP reason phrase."""
|
|
1140
|
+
from http.client import responses
|
|
1141
|
+
return responses.get(self._status_code, "Unknown")
|
|
1142
|
+
|
|
1143
|
+
# -- public methods ----------------------------------------------------
|
|
1144
|
+
|
|
1145
|
+
def json(self, **kwargs: Any) -> Any:
|
|
1146
|
+
"""解析 JSON 响应体。 / Parse response body as JSON.
|
|
1147
|
+
|
|
1148
|
+
Parameters are forwarded to :func:`json.loads`.
|
|
1149
|
+
"""
|
|
1150
|
+
import json
|
|
1151
|
+
return json.loads(self._text, **kwargs)
|
|
1152
|
+
|
|
1153
|
+
def raise_for_status(self) -> None:
|
|
1154
|
+
"""若状态码 ≥ 400,抛出 :exc:`RuntimeError`。
|
|
1155
|
+
|
|
1156
|
+
Raise :exc:`RuntimeError` if the status code indicates an error
|
|
1157
|
+
(4xx client error or 5xx server error).
|
|
1158
|
+
"""
|
|
1159
|
+
if self._status_code >= 400:
|
|
1160
|
+
raise RuntimeError(
|
|
1161
|
+
f"{self._status_code} {self.reason} for url: {self._url or '(unknown)'}"
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
# -- dunder methods ----------------------------------------------------
|
|
1165
|
+
|
|
1166
|
+
def __bool__(self) -> bool:
|
|
1167
|
+
"""``bool(resp)`` → ``resp.ok``。"""
|
|
1168
|
+
return self.ok
|
|
1169
|
+
|
|
1170
|
+
def __repr__(self) -> str:
|
|
1171
|
+
return f"<Response [{self._status_code}]>"
|
|
1172
|
+
|
|
1173
|
+
def __str__(self) -> str:
|
|
1174
|
+
return self.__repr__()
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
# ---------------------------------------------------------------------------
|
|
1178
|
+
# Synchronous Session
|
|
1179
|
+
# ---------------------------------------------------------------------------
|
|
1180
|
+
|
|
1181
|
+
class Session:
|
|
1182
|
+
"""同步 HTTP 客户端,封装 Go tls-client 引擎。
|
|
1183
|
+
|
|
1184
|
+
Synchronous HTTP client wrapping the Go tls-client engine.
|
|
1185
|
+
"""
|
|
1186
|
+
|
|
1187
|
+
defaults: Dict[str, Any]
|
|
1188
|
+
|
|
1189
|
+
def __init__(
|
|
1190
|
+
self,
|
|
1191
|
+
*,
|
|
1192
|
+
# ── 指纹 / 协议 ── / Fingerprint / Protocol ──
|
|
1193
|
+
# TLS 指纹标识 / TLS fingerprint identifier
|
|
1194
|
+
client_identifier: str = "chrome_146",
|
|
1195
|
+
# 强制使用 HTTP/1.1 / Force HTTP/1.1
|
|
1196
|
+
force_http1: bool = False,
|
|
1197
|
+
# 完全禁用 HTTP/3(QUIC) / Completely disable HTTP/3 (QUIC)
|
|
1198
|
+
disable_http3: bool = False,
|
|
1199
|
+
# 启用协议竞速 / Enable protocol racing
|
|
1200
|
+
with_protocol_racing: bool = False,
|
|
1201
|
+
# 随机打乱 TLS 扩展的发送顺序 / Randomize TLS extension send order
|
|
1202
|
+
random_tls_extension_order: bool = True,
|
|
1203
|
+
# ── 超时 / 重定向 ── / Timeout / Redirect ──
|
|
1204
|
+
# 请求整体超时时间(秒) / Total request timeout in seconds
|
|
1205
|
+
timeout: int = 30,
|
|
1206
|
+
# 请求超时时间(毫秒) / Request timeout in milliseconds
|
|
1207
|
+
timeout_milliseconds: int = 0,
|
|
1208
|
+
# 是否自动跟随 HTTP 重定向 / Whether to automatically follow HTTP redirects
|
|
1209
|
+
follow_redirects: bool = False,
|
|
1210
|
+
# ── TLS / 证书 ── / TLS / Cert ──
|
|
1211
|
+
# 是否验证服务端 TLS 证书 / Whether to verify the server TLS certificate
|
|
1212
|
+
verify: bool = True,
|
|
1213
|
+
# 自定义 SNI / Custom SNI
|
|
1214
|
+
server_name_overwrite: Optional[str] = None,
|
|
1215
|
+
# ── 代理 ── / Proxy ──
|
|
1216
|
+
# 代理 URL / Proxy URL
|
|
1217
|
+
proxy: Optional[str] = None,
|
|
1218
|
+
# 绑定到指定的本地 IP 地址 / Bind to a specific local IP address
|
|
1219
|
+
local_address: Optional[str] = None,
|
|
1220
|
+
# ── 请求头控制 ── / Header Control ──
|
|
1221
|
+
# 覆盖 HTTP Host 请求头 / Override the HTTP Host header
|
|
1222
|
+
request_host_override: Optional[str] = None,
|
|
1223
|
+
# HTTP/2 伪头发送顺序 / HTTP/2 pseudo-header send order
|
|
1224
|
+
pseudo_header_order: Optional[List[str]] = None,
|
|
1225
|
+
# HTTP/3 伪头的发送顺序 / HTTP/3 pseudo-header send order
|
|
1226
|
+
h3_pseudo_header_order: Optional[List[str]] = None,
|
|
1227
|
+
# 默认请求头字典 / Default headers dict
|
|
1228
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
1229
|
+
# 代理 CONNECT 隧道请求头字典 / Proxy CONNECT tunnel headers dict
|
|
1230
|
+
connect_headers: Optional[Dict[str, str]] = None,
|
|
1231
|
+
# ── 证书固定 ── / Certificate Pinning ──
|
|
1232
|
+
# SSL 证书固定字典 / SSL certificate pinning dict
|
|
1233
|
+
certificate_pinning_hosts: Optional[Dict[str, List[str]]] = None,
|
|
1234
|
+
# 是否调用默认 Bad-Pin 处理器 / Whether to invoke the default bad-pin handler
|
|
1235
|
+
with_default_bad_pin_handler: bool = False,
|
|
1236
|
+
# ── Cookie ──
|
|
1237
|
+
# 预置 Cookie 字典 / Pre-populated cookie dict
|
|
1238
|
+
request_cookies: Optional[Dict[str, str]] = None,
|
|
1239
|
+
# ── 自定义 TLS ── / Custom TLS ──
|
|
1240
|
+
# 完全自定义的 TLS 客户端配置 (26 fields) / Fully custom TLS client configuration
|
|
1241
|
+
custom_tls_client: Optional[Dict[str, Any]] = None,
|
|
1242
|
+
# 客户端证书 (mTLS) / Client certificates for mTLS
|
|
1243
|
+
client_certificates: Optional[List[Dict[str, bytes]]] = None,
|
|
1244
|
+
# ── 连接池调优 ── / Connection Pool Tuning ──
|
|
1245
|
+
# 全局最大空闲连接数 / Global max idle connections
|
|
1246
|
+
max_idle_connections: int = 0,
|
|
1247
|
+
# 每个 Host 的最大空闲连接数 / Max idle connections per host
|
|
1248
|
+
max_idle_connections_per_host: int = 0,
|
|
1249
|
+
# 每个 Host 的最大总连接数 / Max total connections per host
|
|
1250
|
+
max_connections_per_host: int = 0,
|
|
1251
|
+
# 禁用 HTTP Keep-Alive / Disable HTTP Keep-Alive
|
|
1252
|
+
disable_keep_alives: bool = False,
|
|
1253
|
+
# 禁用响应体自动解压 / Disable automatic response body decompression
|
|
1254
|
+
disable_compression: bool = False,
|
|
1255
|
+
# 空闲连接的最大保持时间(秒) / Max idle time for keep-alive connections
|
|
1256
|
+
idle_conn_timeout_seconds: int = 0,
|
|
1257
|
+
# HTTP 响应头的最大字节数限制 / Max response header bytes limit
|
|
1258
|
+
max_response_header_bytes: int = 0,
|
|
1259
|
+
# Socket 写缓冲区大小(字节) / Socket write buffer size (bytes)
|
|
1260
|
+
write_buffer_size: int = 0,
|
|
1261
|
+
# Socket 读缓冲区大小(字节) / Socket read buffer size (bytes)
|
|
1262
|
+
read_buffer_size: int = 0,
|
|
1263
|
+
# ── IP 协议栈控制 ── / IP Stack Control ──
|
|
1264
|
+
# 禁用 IPv4 / Disable IPv4
|
|
1265
|
+
disable_ipv4: bool = False,
|
|
1266
|
+
# 禁用 IPv6 / Disable IPv6
|
|
1267
|
+
disable_ipv6: bool = False,
|
|
1268
|
+
# ── Cookie ──
|
|
1269
|
+
# 是否允许空 Cookie / Whether to allow empty-value cookies
|
|
1270
|
+
allow_empty_cookies: bool = False,
|
|
1271
|
+
# 完全禁用 Cookie Jar / Completely disable Cookie Jar
|
|
1272
|
+
without_cookie_jar: bool = False,
|
|
1273
|
+
# ── 调试 / 安全 ── / Debug / Safety ──
|
|
1274
|
+
# 是否在 Go 侧捕获 panic / Whether to catch Go panics
|
|
1275
|
+
catch_panics: bool = True,
|
|
1276
|
+
# 启用调试日志输出 / Enable debug log output
|
|
1277
|
+
with_debug: bool = False,
|
|
1278
|
+
) -> None:
|
|
1279
|
+
self.defaults = {
|
|
1280
|
+
"client_identifier": client_identifier,
|
|
1281
|
+
"timeout_seconds": timeout,
|
|
1282
|
+
"timeout_milliseconds": timeout_milliseconds,
|
|
1283
|
+
"follow_redirects": 1 if follow_redirects else 0,
|
|
1284
|
+
"insecure_skip_verify": 0 if verify else 1,
|
|
1285
|
+
"force_http1": 1 if force_http1 else 0,
|
|
1286
|
+
"with_random_tls_extension_order": (
|
|
1287
|
+
1 if random_tls_extension_order else 0
|
|
1288
|
+
),
|
|
1289
|
+
"with_protocol_racing": 1 if with_protocol_racing else 0,
|
|
1290
|
+
"server_name_overwrite": server_name_overwrite,
|
|
1291
|
+
"request_host_override": request_host_override,
|
|
1292
|
+
"local_address": local_address,
|
|
1293
|
+
"proxy": proxy,
|
|
1294
|
+
"pseudo_header_order": pseudo_header_order,
|
|
1295
|
+
"h3_pseudo_header_order": h3_pseudo_header_order,
|
|
1296
|
+
"default_headers": default_headers,
|
|
1297
|
+
"connect_headers": connect_headers,
|
|
1298
|
+
"certificate_pinning_hosts": certificate_pinning_hosts,
|
|
1299
|
+
"with_default_bad_pin_handler": (
|
|
1300
|
+
1 if with_default_bad_pin_handler else 0
|
|
1301
|
+
),
|
|
1302
|
+
"request_cookies": request_cookies,
|
|
1303
|
+
"custom_tls_client": custom_tls_client,
|
|
1304
|
+
"client_certificates": client_certificates,
|
|
1305
|
+
"max_idle_connections": max_idle_connections,
|
|
1306
|
+
"max_idle_connections_per_host": max_idle_connections_per_host,
|
|
1307
|
+
"max_connections_per_host": max_connections_per_host,
|
|
1308
|
+
"disable_keep_alives": 1 if disable_keep_alives else 0,
|
|
1309
|
+
"disable_compression": 1 if disable_compression else 0,
|
|
1310
|
+
"idle_conn_timeout_seconds": idle_conn_timeout_seconds,
|
|
1311
|
+
"max_response_header_bytes": max_response_header_bytes,
|
|
1312
|
+
"write_buffer_size": write_buffer_size,
|
|
1313
|
+
"read_buffer_size": read_buffer_size,
|
|
1314
|
+
"allow_empty_cookies": 1 if allow_empty_cookies else 0,
|
|
1315
|
+
"without_cookie_jar": 1 if without_cookie_jar else 0,
|
|
1316
|
+
"disable_http3": 1 if disable_http3 else 0,
|
|
1317
|
+
"disable_ipv4": 1 if disable_ipv4 else 0,
|
|
1318
|
+
"disable_ipv6": 1 if disable_ipv6 else 0,
|
|
1319
|
+
"catch_panics": 1 if catch_panics else 0,
|
|
1320
|
+
"with_debug": 1 if with_debug else 0,
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
def stream_to_file(
|
|
1324
|
+
self,
|
|
1325
|
+
# HTTP 方法 / HTTP method
|
|
1326
|
+
method: str,
|
|
1327
|
+
# 请求 URL / Request URL
|
|
1328
|
+
url: str,
|
|
1329
|
+
# 输出文件路径 / Output file path
|
|
1330
|
+
output_path: str,
|
|
1331
|
+
*,
|
|
1332
|
+
# 请求头字典 / Request headers dict
|
|
1333
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1334
|
+
# 请求头发送顺序 / Header send order
|
|
1335
|
+
header_order: Optional[List[str]] = None,
|
|
1336
|
+
# 请求体原始字节串 / Request body as raw bytes
|
|
1337
|
+
body: Optional[bytes] = None,
|
|
1338
|
+
# 块大小(字节) / Chunk size per read (bytes)
|
|
1339
|
+
chunk_size: int = 8192,
|
|
1340
|
+
# 可选的 EOF 标记字符串 / Optional EOF marker string
|
|
1341
|
+
eof_marker: Optional[str] = None,
|
|
1342
|
+
**kwargs: Any,
|
|
1343
|
+
) -> Response:
|
|
1344
|
+
"""执行请求并将响应体流式写入磁盘。
|
|
1345
|
+
|
|
1346
|
+
Execute a request and stream the response body to disk.
|
|
1347
|
+
"""
|
|
1348
|
+
|
|
1349
|
+
ffi, _ = _get_ffi()
|
|
1350
|
+
|
|
1351
|
+
keep_alive: list = []
|
|
1352
|
+
c_path = _c_string(ffi, output_path)
|
|
1353
|
+
keep_alive.append(c_path)
|
|
1354
|
+
|
|
1355
|
+
c_eof = _c_string(ffi, eof_marker)
|
|
1356
|
+
if c_eof != ffi.NULL:
|
|
1357
|
+
keep_alive.append(c_eof)
|
|
1358
|
+
|
|
1359
|
+
return self.execute_request(
|
|
1360
|
+
method,
|
|
1361
|
+
url,
|
|
1362
|
+
headers=headers,
|
|
1363
|
+
header_order=header_order,
|
|
1364
|
+
body=body,
|
|
1365
|
+
_stream_output_path=c_path,
|
|
1366
|
+
_stream_output_block_size=chunk_size,
|
|
1367
|
+
_stream_output_eof_symbol=c_eof,
|
|
1368
|
+
_stream_keep_alive=keep_alive,
|
|
1369
|
+
**kwargs,
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
def __enter__(self) -> "Session":
|
|
1373
|
+
return self
|
|
1374
|
+
|
|
1375
|
+
def __exit__(self, *args: Any) -> None:
|
|
1376
|
+
pass
|
|
1377
|
+
|
|
1378
|
+
def execute_request(
|
|
1379
|
+
self,
|
|
1380
|
+
# HTTP 方法 / HTTP method
|
|
1381
|
+
method: str,
|
|
1382
|
+
# 完整的请求 URL / Full request URL including scheme and hostname
|
|
1383
|
+
url: str,
|
|
1384
|
+
*,
|
|
1385
|
+
# 请求头字典 / Request headers dict
|
|
1386
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1387
|
+
# 请求头的发送顺序列表 / Header send-order list
|
|
1388
|
+
header_order: Optional[List[str]] = None,
|
|
1389
|
+
# 请求体原始字节串 / Request body as raw bytes
|
|
1390
|
+
body: Optional[bytes] = None,
|
|
1391
|
+
# ── 每请求覆盖参数 / Per-request overrides ──
|
|
1392
|
+
# 覆盖 TLS 指纹标识 / Override TLS fingerprint identifier
|
|
1393
|
+
client_identifier: Optional[str] = None,
|
|
1394
|
+
# 覆盖超时时间(秒) / Override timeout (seconds)
|
|
1395
|
+
timeout: Optional[int] = None,
|
|
1396
|
+
# 覆盖超时时间(毫秒) / Override timeout (milliseconds)
|
|
1397
|
+
timeout_milliseconds: Optional[int] = None,
|
|
1398
|
+
# 覆盖重定向跟随策略 / Override redirect-following policy
|
|
1399
|
+
follow_redirects: Optional[bool] = None,
|
|
1400
|
+
# 覆盖 TLS 证书校验 / Override TLS certificate verification
|
|
1401
|
+
verify: Optional[bool] = None,
|
|
1402
|
+
# 覆盖 HTTP/1.1 强制开关 / Override force HTTP/1.1
|
|
1403
|
+
force_http1: Optional[bool] = None,
|
|
1404
|
+
# 覆盖 TLS 扩展随机化 / Override TLS extension randomisation
|
|
1405
|
+
random_tls_extension_order: Optional[bool] = None,
|
|
1406
|
+
# 覆盖协议竞速开关 / Override protocol racing
|
|
1407
|
+
with_protocol_racing: Optional[bool] = None,
|
|
1408
|
+
# 覆盖 SNI 主机名 / Override SNI hostname
|
|
1409
|
+
server_name_overwrite: Optional[str] = None,
|
|
1410
|
+
# 覆盖 HTTP Host 请求头 / Override HTTP Host header
|
|
1411
|
+
request_host_override: Optional[str] = None,
|
|
1412
|
+
# 覆盖代理 URL / Override proxy URL
|
|
1413
|
+
proxy: Optional[str] = None,
|
|
1414
|
+
# 覆盖本地绑定地址 / Override local bind address
|
|
1415
|
+
local_address: Optional[str] = None,
|
|
1416
|
+
# 覆盖 HTTP/2 伪头顺序 / Override HTTP/2 pseudo-header order
|
|
1417
|
+
pseudo_header_order: Optional[List[str]] = None,
|
|
1418
|
+
# 覆盖 HTTP/3 伪头顺序 / Override HTTP/3 pseudo-header order
|
|
1419
|
+
h3_pseudo_header_order: Optional[List[str]] = None,
|
|
1420
|
+
# 覆盖默认请求头字典 / Override default headers dict
|
|
1421
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
1422
|
+
# 覆盖代理 CONNECT 隧道请求头 / Override proxy CONNECT tunnel headers
|
|
1423
|
+
connect_headers: Optional[Dict[str, str]] = None,
|
|
1424
|
+
# 覆盖 SSL 证书固定字典 / Override SSL certificate pinning dict
|
|
1425
|
+
certificate_pinning_hosts: Optional[Dict[str, List[str]]] = None,
|
|
1426
|
+
# 覆盖 Bad-Pin 处理器开关 / Override default bad-pin handler toggle
|
|
1427
|
+
with_default_bad_pin_handler: Optional[bool] = None,
|
|
1428
|
+
# 覆盖预置 Cookie 字典 / Override pre-populated cookie dict
|
|
1429
|
+
request_cookies: Optional[Dict[str, str]] = None,
|
|
1430
|
+
# 覆盖客户端证书列表 / Override client certificate list
|
|
1431
|
+
client_certificates: Optional[List[Dict[str, bytes]]] = None,
|
|
1432
|
+
# 覆盖自定义 TLS 客户端配置 / Override custom TLS client configuration
|
|
1433
|
+
custom_tls_client: Optional[Dict[str, Any]] = None,
|
|
1434
|
+
# 覆盖全局最大空闲连接数 / Override global max idle connections
|
|
1435
|
+
max_idle_connections: Optional[int] = None,
|
|
1436
|
+
# 覆盖每 Host 最大空闲连接数 / Override max idle connections per host
|
|
1437
|
+
max_idle_connections_per_host: Optional[int] = None,
|
|
1438
|
+
# 覆盖每 Host 最大总连接数 / Override max total connections per host
|
|
1439
|
+
max_connections_per_host: Optional[int] = None,
|
|
1440
|
+
# 覆盖 Keep-Alive 禁用 / Override disable Keep-Alive
|
|
1441
|
+
disable_keep_alives: Optional[bool] = None,
|
|
1442
|
+
# 覆盖压缩禁用 / Override disable compression
|
|
1443
|
+
disable_compression: Optional[bool] = None,
|
|
1444
|
+
# 覆盖空闲连接超时 / Override idle connection timeout
|
|
1445
|
+
idle_conn_timeout_seconds: Optional[int] = None,
|
|
1446
|
+
# 覆盖响应头最大字节数 / Override max response header bytes
|
|
1447
|
+
max_response_header_bytes: Optional[int] = None,
|
|
1448
|
+
# 覆盖写缓冲区大小 / Override write buffer size
|
|
1449
|
+
write_buffer_size: Optional[int] = None,
|
|
1450
|
+
# 覆盖读缓冲区大小 / Override read buffer size
|
|
1451
|
+
read_buffer_size: Optional[int] = None,
|
|
1452
|
+
# 覆盖空 Cookie 允许 / Override allow-empty-cookies
|
|
1453
|
+
allow_empty_cookies: Optional[bool] = None,
|
|
1454
|
+
# 覆盖禁用 Cookie Jar / Override disable Cookie Jar
|
|
1455
|
+
without_cookie_jar: Optional[bool] = None,
|
|
1456
|
+
# 覆盖 HTTP/3 禁用 / Override disable HTTP/3
|
|
1457
|
+
disable_http3: Optional[bool] = None,
|
|
1458
|
+
# 覆盖 IPv4 禁用 / Override disable IPv4
|
|
1459
|
+
disable_ipv4: Optional[bool] = None,
|
|
1460
|
+
# 覆盖 IPv6 禁用 / Override disable IPv6
|
|
1461
|
+
disable_ipv6: Optional[bool] = None,
|
|
1462
|
+
# 覆盖 panic 捕获 / Override catch-panics
|
|
1463
|
+
catch_panics: Optional[bool] = None,
|
|
1464
|
+
# 覆盖调试日志 / Override debug logging
|
|
1465
|
+
with_debug: Optional[bool] = None,
|
|
1466
|
+
**kwargs: Any,
|
|
1467
|
+
) -> Response:
|
|
1468
|
+
"""通过 Go 引擎执行单次 HTTP 请求。
|
|
1469
|
+
|
|
1470
|
+
Execute a single HTTP request through the Go engine.
|
|
1471
|
+
"""
|
|
1472
|
+
ffi, lib = _get_ffi()
|
|
1473
|
+
|
|
1474
|
+
def _val(name: str, override, as_bool: bool = False):
|
|
1475
|
+
v = override if override is not None else self.defaults[name]
|
|
1476
|
+
if as_bool:
|
|
1477
|
+
return 1 if v else 0
|
|
1478
|
+
return v
|
|
1479
|
+
|
|
1480
|
+
keep_alive: list = []
|
|
1481
|
+
|
|
1482
|
+
# ---- build C HttpHeader array -------------------------------------
|
|
1483
|
+
hdr_ptr, hdr_len = _build_headers(ffi, headers, keep_alive)
|
|
1484
|
+
|
|
1485
|
+
# ---- build header_order array -------------------------------------
|
|
1486
|
+
ho_ptr, ho_len = _build_string_array(ffi, header_order, keep_alive)
|
|
1487
|
+
|
|
1488
|
+
# ---- build pseudo_header_order array ------------------------------
|
|
1489
|
+
ph_ptr, ph_len = _build_string_array(
|
|
1490
|
+
ffi, _val("pseudo_header_order", pseudo_header_order), keep_alive
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
# ---- build h3_pseudo_header_order array ---------------------------
|
|
1494
|
+
h3ph_ptr, h3ph_len = _build_string_array(
|
|
1495
|
+
ffi, _val("h3_pseudo_header_order", h3_pseudo_header_order), keep_alive
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
# ---- build default_headers array ----------------------------------
|
|
1499
|
+
dh_ptr, dh_len = _build_headers(
|
|
1500
|
+
ffi, _val("default_headers", default_headers), keep_alive
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
# ---- build connect_headers array ----------------------------------
|
|
1504
|
+
ch_ptr, ch_len = _build_headers(
|
|
1505
|
+
ffi, _val("connect_headers", connect_headers), keep_alive
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
# ---- build certificate_pinning_hosts array -------------------------
|
|
1509
|
+
cp_ptr, cp_len = _build_pin_entries(
|
|
1510
|
+
ffi, _val("certificate_pinning_hosts", certificate_pinning_hosts), keep_alive
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
# ---- build request_cookies array -----------------------------------
|
|
1514
|
+
rc_ptr, rc_len = _build_headers(
|
|
1515
|
+
ffi, _val("request_cookies", request_cookies), keep_alive
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
# ---- build client_certificates array --------------------------------
|
|
1519
|
+
cc_ptr, cc_len = _build_client_certificates(
|
|
1520
|
+
ffi, _val("client_certificates", client_certificates), keep_alive
|
|
1521
|
+
)
|
|
1522
|
+
|
|
1523
|
+
# ---- build custom_tls_client --------------------------------------
|
|
1524
|
+
ctc_ptr = _build_custom_tls_client(
|
|
1525
|
+
ffi, _val("custom_tls_client", custom_tls_client), keep_alive
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
# ---- build body ---------------------------------------------------
|
|
1529
|
+
if body is not None:
|
|
1530
|
+
c_body = ffi.new("char[]", body)
|
|
1531
|
+
keep_alive.append(c_body)
|
|
1532
|
+
body_ptr = c_body
|
|
1533
|
+
body_len = len(body)
|
|
1534
|
+
else:
|
|
1535
|
+
body_ptr = ffi.NULL
|
|
1536
|
+
body_len = 0
|
|
1537
|
+
|
|
1538
|
+
# ---- build RequestOptions -----------------------------------------
|
|
1539
|
+
opts = ffi.new("RequestOptions *")
|
|
1540
|
+
keep_alive.append(opts)
|
|
1541
|
+
|
|
1542
|
+
c_method = ffi.new("char[]", method.encode("utf-8"))
|
|
1543
|
+
c_url = ffi.new("char[]", url.encode("utf-8"))
|
|
1544
|
+
keep_alive.extend([c_method, c_url])
|
|
1545
|
+
|
|
1546
|
+
opts.method = c_method
|
|
1547
|
+
opts.url = c_url
|
|
1548
|
+
opts.body = body_ptr
|
|
1549
|
+
opts.body_len = body_len
|
|
1550
|
+
|
|
1551
|
+
c_proxy = _c_string(ffi, _val("proxy", proxy))
|
|
1552
|
+
c_ci = _c_string(ffi, _val("client_identifier", client_identifier))
|
|
1553
|
+
c_sni = _c_string(ffi, _val("server_name_overwrite", server_name_overwrite))
|
|
1554
|
+
c_host_override = _c_string(ffi, _val("request_host_override", request_host_override))
|
|
1555
|
+
c_local_addr = _c_string(ffi, _val("local_address", local_address))
|
|
1556
|
+
for c in (c_proxy, c_ci, c_sni, c_host_override, c_local_addr):
|
|
1557
|
+
if c != ffi.NULL:
|
|
1558
|
+
keep_alive.append(c)
|
|
1559
|
+
|
|
1560
|
+
opts.proxy = c_proxy
|
|
1561
|
+
opts.client_identifier = c_ci
|
|
1562
|
+
opts.headers = hdr_ptr
|
|
1563
|
+
opts.headers_len = hdr_len
|
|
1564
|
+
opts.header_order = ho_ptr
|
|
1565
|
+
opts.header_order_len = ho_len
|
|
1566
|
+
opts.pseudo_header_order = ph_ptr
|
|
1567
|
+
opts.pseudo_header_order_len = ph_len
|
|
1568
|
+
opts.h3_pseudo_header_order = h3ph_ptr
|
|
1569
|
+
opts.h3_pseudo_header_order_len = h3ph_len
|
|
1570
|
+
opts.default_headers = dh_ptr
|
|
1571
|
+
opts.default_headers_len = dh_len
|
|
1572
|
+
opts.connect_headers = ch_ptr
|
|
1573
|
+
opts.connect_headers_len = ch_len
|
|
1574
|
+
opts.certificate_pinning_hosts = cp_ptr
|
|
1575
|
+
opts.certificate_pinning_hosts_len = cp_len
|
|
1576
|
+
opts.with_default_bad_pin_handler = _val(
|
|
1577
|
+
"with_default_bad_pin_handler", with_default_bad_pin_handler, True
|
|
1578
|
+
)
|
|
1579
|
+
opts.request_cookies = rc_ptr
|
|
1580
|
+
opts.request_cookies_len = rc_len
|
|
1581
|
+
opts.client_certificates = cc_ptr
|
|
1582
|
+
opts.client_certificates_len = cc_len
|
|
1583
|
+
opts.custom_tls_client = ctc_ptr
|
|
1584
|
+
|
|
1585
|
+
opts.timeout_seconds = _val("timeout_seconds", timeout)
|
|
1586
|
+
opts.timeout_milliseconds = _val("timeout_milliseconds", timeout_milliseconds)
|
|
1587
|
+
opts.follow_redirects = _val("follow_redirects", follow_redirects, True)
|
|
1588
|
+
if verify is not None:
|
|
1589
|
+
opts.insecure_skip_verify = 0 if verify else 1
|
|
1590
|
+
else:
|
|
1591
|
+
opts.insecure_skip_verify = self.defaults["insecure_skip_verify"]
|
|
1592
|
+
opts.force_http1 = _val("force_http1", force_http1, True)
|
|
1593
|
+
opts.with_random_tls_extension_order = _val(
|
|
1594
|
+
"with_random_tls_extension_order", random_tls_extension_order, True
|
|
1595
|
+
)
|
|
1596
|
+
opts.with_protocol_racing = _val(
|
|
1597
|
+
"with_protocol_racing", with_protocol_racing, True
|
|
1598
|
+
)
|
|
1599
|
+
opts.server_name_overwrite = c_sni
|
|
1600
|
+
opts.request_host_override = c_host_override
|
|
1601
|
+
opts.local_address = c_local_addr
|
|
1602
|
+
opts.max_idle_connections = _val("max_idle_connections", max_idle_connections)
|
|
1603
|
+
opts.max_idle_connections_per_host = _val(
|
|
1604
|
+
"max_idle_connections_per_host", max_idle_connections_per_host
|
|
1605
|
+
)
|
|
1606
|
+
opts.max_connections_per_host = _val(
|
|
1607
|
+
"max_connections_per_host", max_connections_per_host
|
|
1608
|
+
)
|
|
1609
|
+
opts.disable_keep_alives = _val("disable_keep_alives", disable_keep_alives, True)
|
|
1610
|
+
opts.disable_compression = _val("disable_compression", disable_compression, True)
|
|
1611
|
+
opts.idle_conn_timeout_seconds = _val(
|
|
1612
|
+
"idle_conn_timeout_seconds", idle_conn_timeout_seconds
|
|
1613
|
+
)
|
|
1614
|
+
opts.max_response_header_bytes = _val(
|
|
1615
|
+
"max_response_header_bytes", max_response_header_bytes
|
|
1616
|
+
)
|
|
1617
|
+
opts.write_buffer_size = _val("write_buffer_size", write_buffer_size)
|
|
1618
|
+
opts.read_buffer_size = _val("read_buffer_size", read_buffer_size)
|
|
1619
|
+
opts.allow_empty_cookies = _val("allow_empty_cookies", allow_empty_cookies, True)
|
|
1620
|
+
opts.without_cookie_jar = _val("without_cookie_jar", without_cookie_jar, True)
|
|
1621
|
+
opts.disable_http3 = _val("disable_http3", disable_http3, True)
|
|
1622
|
+
opts.disable_ipv4 = _val("disable_ipv4", disable_ipv4, True)
|
|
1623
|
+
opts.disable_ipv6 = _val("disable_ipv6", disable_ipv6, True)
|
|
1624
|
+
opts.catch_panics = _val("catch_panics", catch_panics, True)
|
|
1625
|
+
opts.with_debug = _val("with_debug", with_debug, True)
|
|
1626
|
+
|
|
1627
|
+
# ---- streaming fields (injected by stream_to_file) -----------------
|
|
1628
|
+
stream_path = kwargs.pop("_stream_output_path", ffi.NULL)
|
|
1629
|
+
stream_bs = kwargs.pop("_stream_output_block_size", 0)
|
|
1630
|
+
stream_eof = kwargs.pop("_stream_output_eof_symbol", ffi.NULL)
|
|
1631
|
+
stream_keep = kwargs.pop("_stream_keep_alive", None)
|
|
1632
|
+
if stream_keep is not None:
|
|
1633
|
+
keep_alive.extend(stream_keep)
|
|
1634
|
+
opts.stream_output_path = stream_path
|
|
1635
|
+
opts.stream_output_block_size = stream_bs
|
|
1636
|
+
opts.stream_output_eof_symbol = stream_eof
|
|
1637
|
+
|
|
1638
|
+
# ---- call Go ------------------------------------------------------
|
|
1639
|
+
raw_res = lib.ExecuteRequest(opts)
|
|
1640
|
+
|
|
1641
|
+
if raw_res == ffi.NULL:
|
|
1642
|
+
raise RuntimeError("Go engine returned NULL – likely memory allocation failure")
|
|
1643
|
+
|
|
1644
|
+
raw_res = ffi.gc(raw_res, lib.FreeResponse)
|
|
1645
|
+
|
|
1646
|
+
if raw_res.err_msg != ffi.NULL:
|
|
1647
|
+
err = ffi.string(raw_res.err_msg).decode("utf-8", errors="replace")
|
|
1648
|
+
raise RuntimeError(err)
|
|
1649
|
+
|
|
1650
|
+
return _unpack_c_response(ffi, raw_res)
|
|
1651
|
+
|
|
1652
|
+
def typed_request(self, req: Request) -> Response:
|
|
1653
|
+
"""使用 :class:`Request` 强类型对象执行 HTTP 请求。
|
|
1654
|
+
|
|
1655
|
+
Execute an HTTP request using a :class:`Request` strongly-typed object.
|
|
1656
|
+
"""
|
|
1657
|
+
method = req["method"]
|
|
1658
|
+
url = req["url"]
|
|
1659
|
+
kwargs = {k: v for k, v in req.items() if k not in ("method", "url")}
|
|
1660
|
+
return self.execute_request(method, url, **kwargs)
|
|
1661
|
+
|
|
1662
|
+
def get(self, url: str, *, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Response:
|
|
1663
|
+
return self.execute_request("GET", url, headers=headers, **kwargs)
|
|
1664
|
+
|
|
1665
|
+
def post(self, url: str, *, headers: Optional[Dict[str, str]] = None, body: Optional[bytes] = None, **kwargs: Any) -> Response:
|
|
1666
|
+
return self.execute_request("POST", url, headers=headers, body=body, **kwargs)
|
|
1667
|
+
|
|
1668
|
+
def head(self, url: str, *, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Response:
|
|
1669
|
+
return self.execute_request("HEAD", url, headers=headers, **kwargs)
|
|
1670
|
+
|
|
1671
|
+
def put(self, url: str, *, headers: Optional[Dict[str, str]] = None, body: Optional[bytes] = None, **kwargs: Any) -> Response:
|
|
1672
|
+
return self.execute_request("PUT", url, headers=headers, body=body, **kwargs)
|
|
1673
|
+
|
|
1674
|
+
def delete(self, url: str, *, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Response:
|
|
1675
|
+
return self.execute_request("DELETE", url, headers=headers, **kwargs)
|
|
1676
|
+
|
|
1677
|
+
def patch(self, url: str, *, headers: Optional[Dict[str, str]] = None, body: Optional[bytes] = None, **kwargs: Any) -> Response:
|
|
1678
|
+
return self.execute_request("PATCH", url, headers=headers, body=body, **kwargs)
|
|
1679
|
+
|
|
1680
|
+
@staticmethod
|
|
1681
|
+
def clear_client_pool() -> None:
|
|
1682
|
+
"""Close all idle connections in the global Go client pool."""
|
|
1683
|
+
_, lib = _get_ffi()
|
|
1684
|
+
lib.ClearClientPool()
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
# ---------------------------------------------------------------------------
|
|
1688
|
+
# Async callback bridge — Go goroutine → Python CFFI callback → asyncio Future
|
|
1689
|
+
# ---------------------------------------------------------------------------
|
|
1690
|
+
|
|
1691
|
+
# Registry of pending async requests: request_id (int) → asyncio.Future
|
|
1692
|
+
_pending_requests: Dict[int, asyncio.Future] = {}
|
|
1693
|
+
_pending_lock = threading.Lock()
|
|
1694
|
+
_request_counter = 0
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def _next_request_id() -> int:
|
|
1698
|
+
global _request_counter
|
|
1699
|
+
_request_counter += 1
|
|
1700
|
+
return _request_counter
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _make_async_callback(ffi, lib):
|
|
1704
|
+
"""Create a CFFI callback that Go goroutines invoke with results.
|
|
1705
|
+
|
|
1706
|
+
The callback is called from a Go-managed OS thread (not the Python main
|
|
1707
|
+
thread). CFFI internally calls PyGILState_Ensure to safely acquire the
|
|
1708
|
+
GIL before entering Python code.
|
|
1709
|
+
|
|
1710
|
+
Lifecycle (per the 9-step user blueprint):
|
|
1711
|
+
1. Python calls RequestAsync → Go deep-copies → returns immediately
|
|
1712
|
+
2. Go goroutine executes HTTP request
|
|
1713
|
+
3. Go allocates CResponseResult on C heap via C.malloc
|
|
1714
|
+
4. Go calls this callback with (request_id, CResponseResult*)
|
|
1715
|
+
5. Python unpacks CResponseResult → Response object
|
|
1716
|
+
6. Python calls FreeResponse to release C memory
|
|
1717
|
+
7. Python uses loop.call_soon_threadsafe to resolve the Future
|
|
1718
|
+
"""
|
|
1719
|
+
|
|
1720
|
+
@ffi.callback("void(uintptr_t, ResponseResult*)")
|
|
1721
|
+
def _on_async_response(request_id: int, raw_res: Any) -> None:
|
|
1722
|
+
# The ENTIRE callback body MUST be wrapped in try/except Exception.
|
|
1723
|
+
# This callback executes on a Go-managed OS thread. If any Python
|
|
1724
|
+
# exception escapes uncaught, the Python interpreter crashes with
|
|
1725
|
+
# Fatal Python error: Segmentation fault (Constraint 3).
|
|
1726
|
+
try:
|
|
1727
|
+
# Step 5-6: Unpack response
|
|
1728
|
+
if raw_res == ffi.NULL or raw_res.err_msg != ffi.NULL:
|
|
1729
|
+
err = "Go engine returned NULL"
|
|
1730
|
+
if raw_res != ffi.NULL and raw_res.err_msg != ffi.NULL:
|
|
1731
|
+
err = ffi.string(raw_res.err_msg).decode("utf-8", errors="replace")
|
|
1732
|
+
response: Any = RuntimeError(err)
|
|
1733
|
+
else:
|
|
1734
|
+
response = _unpack_c_response(ffi, raw_res)
|
|
1735
|
+
except Exception as exc:
|
|
1736
|
+
response = exc
|
|
1737
|
+
finally:
|
|
1738
|
+
# Step 6: Free C memory in all code paths (success, error, panic)
|
|
1739
|
+
if raw_res != ffi.NULL:
|
|
1740
|
+
lib.FreeResponse(raw_res)
|
|
1741
|
+
|
|
1742
|
+
# Step 9: Schedule Future resolution on the event loop thread.
|
|
1743
|
+
# Must use call_soon_threadsafe — direct set_result/set_exception
|
|
1744
|
+
# from a non-event-loop thread is NOT thread-safe (Constraint 4).
|
|
1745
|
+
try:
|
|
1746
|
+
with _pending_lock:
|
|
1747
|
+
future = _pending_requests.pop(request_id, None)
|
|
1748
|
+
|
|
1749
|
+
if future is None:
|
|
1750
|
+
return # request was cancelled or timed out
|
|
1751
|
+
|
|
1752
|
+
if not future.done():
|
|
1753
|
+
loop = future.get_loop()
|
|
1754
|
+
if isinstance(response, Exception):
|
|
1755
|
+
loop.call_soon_threadsafe(future.set_exception, response)
|
|
1756
|
+
else:
|
|
1757
|
+
loop.call_soon_threadsafe(future.set_result, response)
|
|
1758
|
+
except Exception:
|
|
1759
|
+
# If even Future resolution fails, there is nothing we can do —
|
|
1760
|
+
# the event loop may be shut down. Silently swallow to prevent
|
|
1761
|
+
# a process crash.
|
|
1762
|
+
pass
|
|
1763
|
+
|
|
1764
|
+
return _on_async_response
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
# Lazy-initialised callback — must be created after ffi is loaded and kept alive.
|
|
1768
|
+
# Protected by a lock to prevent race conditions when multiple AsyncSession
|
|
1769
|
+
# instances are created concurrently from different threads.
|
|
1770
|
+
_async_callback = None
|
|
1771
|
+
_async_callback_lock = threading.Lock()
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
def _get_async_callback(ffi, lib):
|
|
1775
|
+
global _async_callback
|
|
1776
|
+
if _async_callback is None:
|
|
1777
|
+
with _async_callback_lock:
|
|
1778
|
+
if _async_callback is None:
|
|
1779
|
+
_async_callback = _make_async_callback(ffi, lib)
|
|
1780
|
+
return _async_callback
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
class AsyncSession:
|
|
1784
|
+
"""True async HTTP client — no thread pool, no blocking.
|
|
1785
|
+
|
|
1786
|
+
Uses Go goroutines + CFFI callbacks + asyncio Future:
|
|
1787
|
+
- Python calls RequestAsync (returns instantly after Go deep-copies)
|
|
1788
|
+
- Go goroutine executes HTTP I/O in the background
|
|
1789
|
+
- Go invokes CFFI callback when done
|
|
1790
|
+
- Python resolves asyncio.Future via call_soon_threadsafe
|
|
1791
|
+
|
|
1792
|
+
Compatible with uvloop / winloop for maximum event-loop performance.
|
|
1793
|
+
"""
|
|
1794
|
+
|
|
1795
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
1796
|
+
self._session = Session(**kwargs)
|
|
1797
|
+
|
|
1798
|
+
async def __aenter__(self) -> "AsyncSession":
|
|
1799
|
+
return self
|
|
1800
|
+
|
|
1801
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
1802
|
+
pass
|
|
1803
|
+
|
|
1804
|
+
async def _execute_async(self, method: str, url: str, **kwargs: Any) -> Response:
|
|
1805
|
+
ffi, lib = _get_ffi()
|
|
1806
|
+
callback = _get_async_callback(ffi, lib)
|
|
1807
|
+
loop = asyncio.get_running_loop()
|
|
1808
|
+
|
|
1809
|
+
# Build RequestOptions (same as sync path)
|
|
1810
|
+
keep_alive: list = []
|
|
1811
|
+
opts = ffi.new("RequestOptions *")
|
|
1812
|
+
keep_alive.append(opts)
|
|
1813
|
+
|
|
1814
|
+
c_method = ffi.new("char[]", method.encode("utf-8"))
|
|
1815
|
+
c_url = ffi.new("char[]", url.encode("utf-8"))
|
|
1816
|
+
keep_alive.extend([c_method, c_url])
|
|
1817
|
+
opts.method = c_method
|
|
1818
|
+
opts.url = c_url
|
|
1819
|
+
|
|
1820
|
+
# Headers
|
|
1821
|
+
headers = kwargs.pop("headers", None)
|
|
1822
|
+
hdr_ptr, hdr_len = _build_headers(ffi, headers, keep_alive)
|
|
1823
|
+
opts.headers = hdr_ptr
|
|
1824
|
+
opts.headers_len = hdr_len
|
|
1825
|
+
|
|
1826
|
+
header_order = kwargs.pop("header_order", None)
|
|
1827
|
+
ho_ptr, ho_len = _build_string_array(ffi, header_order, keep_alive)
|
|
1828
|
+
opts.header_order = ho_ptr
|
|
1829
|
+
opts.header_order_len = ho_len
|
|
1830
|
+
|
|
1831
|
+
# Body
|
|
1832
|
+
body = kwargs.pop("body", None)
|
|
1833
|
+
if body is not None:
|
|
1834
|
+
c_body = ffi.new("char[]", body)
|
|
1835
|
+
keep_alive.append(c_body)
|
|
1836
|
+
opts.body = c_body
|
|
1837
|
+
opts.body_len = len(body)
|
|
1838
|
+
|
|
1839
|
+
# Overrides (use defaults from the session, overridden by kwargs)
|
|
1840
|
+
def _val(name: str, as_bool: bool = False):
|
|
1841
|
+
v = kwargs.pop(name, None)
|
|
1842
|
+
if v is None:
|
|
1843
|
+
v = self._session.defaults[name]
|
|
1844
|
+
if as_bool:
|
|
1845
|
+
return 1 if v else 0
|
|
1846
|
+
return v
|
|
1847
|
+
|
|
1848
|
+
opts.timeout_seconds = _val("timeout_seconds")
|
|
1849
|
+
opts.timeout_milliseconds = _val("timeout_milliseconds")
|
|
1850
|
+
opts.follow_redirects = _val("follow_redirects", True)
|
|
1851
|
+
opts.insecure_skip_verify = _val("insecure_skip_verify", True)
|
|
1852
|
+
opts.force_http1 = _val("force_http1", True)
|
|
1853
|
+
opts.with_random_tls_extension_order = _val("with_random_tls_extension_order", True)
|
|
1854
|
+
opts.with_protocol_racing = _val("with_protocol_racing", True)
|
|
1855
|
+
opts.max_idle_connections = _val("max_idle_connections")
|
|
1856
|
+
opts.max_idle_connections_per_host = _val("max_idle_connections_per_host")
|
|
1857
|
+
opts.max_connections_per_host = _val("max_connections_per_host")
|
|
1858
|
+
opts.disable_keep_alives = _val("disable_keep_alives", True)
|
|
1859
|
+
opts.disable_compression = _val("disable_compression", True)
|
|
1860
|
+
opts.idle_conn_timeout_seconds = _val("idle_conn_timeout_seconds")
|
|
1861
|
+
opts.max_response_header_bytes = _val("max_response_header_bytes")
|
|
1862
|
+
opts.write_buffer_size = _val("write_buffer_size")
|
|
1863
|
+
opts.read_buffer_size = _val("read_buffer_size")
|
|
1864
|
+
opts.allow_empty_cookies = _val("allow_empty_cookies", True)
|
|
1865
|
+
opts.without_cookie_jar = _val("without_cookie_jar", True)
|
|
1866
|
+
opts.disable_http3 = _val("disable_http3", True)
|
|
1867
|
+
opts.disable_ipv4 = _val("disable_ipv4", True)
|
|
1868
|
+
opts.disable_ipv6 = _val("disable_ipv6", True)
|
|
1869
|
+
opts.catch_panics = _val("catch_panics", True)
|
|
1870
|
+
opts.with_debug = _val("with_debug", True)
|
|
1871
|
+
|
|
1872
|
+
# String fields
|
|
1873
|
+
for name, field in [
|
|
1874
|
+
("client_identifier", "client_identifier"),
|
|
1875
|
+
("proxy", "proxy"),
|
|
1876
|
+
("server_name_overwrite", "server_name_overwrite"),
|
|
1877
|
+
("request_host_override", "request_host_override"),
|
|
1878
|
+
("local_address", "local_address"),
|
|
1879
|
+
]:
|
|
1880
|
+
c_val = _c_string(ffi, _val(name))
|
|
1881
|
+
setattr(opts, field, c_val)
|
|
1882
|
+
if c_val != ffi.NULL:
|
|
1883
|
+
keep_alive.append(c_val)
|
|
1884
|
+
|
|
1885
|
+
# Pseudo-header orders
|
|
1886
|
+
pho = _val("pseudo_header_order")
|
|
1887
|
+
ph_ptr, ph_len = _build_string_array(ffi, pho, keep_alive)
|
|
1888
|
+
opts.pseudo_header_order = ph_ptr
|
|
1889
|
+
opts.pseudo_header_order_len = ph_len
|
|
1890
|
+
|
|
1891
|
+
h3pho = _val("h3_pseudo_header_order")
|
|
1892
|
+
h3ph_ptr, h3ph_len = _build_string_array(ffi, h3pho, keep_alive)
|
|
1893
|
+
opts.h3_pseudo_header_order = h3ph_ptr
|
|
1894
|
+
opts.h3_pseudo_header_order_len = h3ph_len
|
|
1895
|
+
|
|
1896
|
+
# Default/connect headers
|
|
1897
|
+
dh = _val("default_headers")
|
|
1898
|
+
dh_ptr, dh_len = _build_headers(ffi, dh, keep_alive)
|
|
1899
|
+
opts.default_headers = dh_ptr
|
|
1900
|
+
opts.default_headers_len = dh_len
|
|
1901
|
+
|
|
1902
|
+
ch = _val("connect_headers")
|
|
1903
|
+
ch_ptr, ch_len = _build_headers(ffi, ch, keep_alive)
|
|
1904
|
+
opts.connect_headers = ch_ptr
|
|
1905
|
+
opts.connect_headers_len = ch_len
|
|
1906
|
+
|
|
1907
|
+
# Certificate pinning
|
|
1908
|
+
cp = _val("certificate_pinning_hosts")
|
|
1909
|
+
cp_ptr, cp_len = _build_pin_entries(ffi, cp, keep_alive)
|
|
1910
|
+
opts.certificate_pinning_hosts = cp_ptr
|
|
1911
|
+
opts.certificate_pinning_hosts_len = cp_len
|
|
1912
|
+
opts.with_default_bad_pin_handler = _val("with_default_bad_pin_handler", True)
|
|
1913
|
+
|
|
1914
|
+
# Request cookies
|
|
1915
|
+
rc = _val("request_cookies")
|
|
1916
|
+
rc_ptr, rc_len = _build_headers(ffi, rc, keep_alive)
|
|
1917
|
+
opts.request_cookies = rc_ptr
|
|
1918
|
+
opts.request_cookies_len = rc_len
|
|
1919
|
+
|
|
1920
|
+
# Client certificates
|
|
1921
|
+
cc = _val("client_certificates")
|
|
1922
|
+
cc_ptr, cc_len = _build_client_certificates(ffi, cc, keep_alive)
|
|
1923
|
+
opts.client_certificates = cc_ptr
|
|
1924
|
+
opts.client_certificates_len = cc_len
|
|
1925
|
+
|
|
1926
|
+
# Custom TLS client
|
|
1927
|
+
ctc = _val("custom_tls_client")
|
|
1928
|
+
ctc_ptr = _build_custom_tls_client(ffi, ctc, keep_alive)
|
|
1929
|
+
opts.custom_tls_client = ctc_ptr
|
|
1930
|
+
|
|
1931
|
+
# ---- streaming fields (injected by stream_to_file) -----------------
|
|
1932
|
+
stream_path = kwargs.pop("_stream_output_path", ffi.NULL)
|
|
1933
|
+
stream_bs = kwargs.pop("_stream_output_block_size", 0)
|
|
1934
|
+
stream_eof = kwargs.pop("_stream_output_eof_symbol", ffi.NULL)
|
|
1935
|
+
stream_keep = kwargs.pop("_stream_keep_alive", None)
|
|
1936
|
+
if stream_keep is not None:
|
|
1937
|
+
keep_alive.extend(stream_keep)
|
|
1938
|
+
opts.stream_output_path = stream_path
|
|
1939
|
+
opts.stream_output_block_size = stream_bs
|
|
1940
|
+
opts.stream_output_eof_symbol = stream_eof
|
|
1941
|
+
|
|
1942
|
+
# Register Future
|
|
1943
|
+
request_id = _next_request_id()
|
|
1944
|
+
future: asyncio.Future = loop.create_future()
|
|
1945
|
+
with _pending_lock:
|
|
1946
|
+
_pending_requests[request_id] = future
|
|
1947
|
+
|
|
1948
|
+
# Step 1-2: Call RequestAsync — Go deep-copies, dispatches goroutine,
|
|
1949
|
+
# returns immediately. Python can free keep_alive memory after return.
|
|
1950
|
+
ret = lib.RequestAsync(opts, request_id, callback)
|
|
1951
|
+
|
|
1952
|
+
if ret != 0:
|
|
1953
|
+
with _pending_lock:
|
|
1954
|
+
_pending_requests.pop(request_id, None)
|
|
1955
|
+
raise RuntimeError("RequestAsync failed — opts or callback is nil")
|
|
1956
|
+
|
|
1957
|
+
# Await the Future — event loop is free to run other tasks
|
|
1958
|
+
return await future
|
|
1959
|
+
|
|
1960
|
+
async def stream_to_file(
|
|
1961
|
+
self,
|
|
1962
|
+
# HTTP 方法 / HTTP method
|
|
1963
|
+
method: str,
|
|
1964
|
+
# 请求 URL / Request URL
|
|
1965
|
+
url: str,
|
|
1966
|
+
# 输出文件路径 / Output file path
|
|
1967
|
+
output_path: str,
|
|
1968
|
+
*,
|
|
1969
|
+
# 请求头字典 / Request headers dict
|
|
1970
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1971
|
+
# 请求头发送顺序 / Header send order
|
|
1972
|
+
header_order: Optional[List[str]] = None,
|
|
1973
|
+
# 请求体原始字节串 / Request body as raw bytes
|
|
1974
|
+
body: Optional[bytes] = None,
|
|
1975
|
+
# 块大小(字节) / Chunk size per read (bytes)
|
|
1976
|
+
chunk_size: int = 8192,
|
|
1977
|
+
# 可选的 EOF 标记字符串 / Optional EOF marker string
|
|
1978
|
+
eof_marker: Optional[str] = None,
|
|
1979
|
+
**kwargs: Any,
|
|
1980
|
+
) -> Response:
|
|
1981
|
+
"""执行请求并将响应体流式写入磁盘(异步)。
|
|
1982
|
+
|
|
1983
|
+
Execute a request and stream the response body to disk (async).
|
|
1984
|
+
"""
|
|
1985
|
+
ffi, _ = _get_ffi()
|
|
1986
|
+
|
|
1987
|
+
keep_alive: list = []
|
|
1988
|
+
c_path = _c_string(ffi, output_path)
|
|
1989
|
+
keep_alive.append(c_path)
|
|
1990
|
+
|
|
1991
|
+
c_eof = _c_string(ffi, eof_marker)
|
|
1992
|
+
if c_eof != ffi.NULL:
|
|
1993
|
+
keep_alive.append(c_eof)
|
|
1994
|
+
|
|
1995
|
+
return await self._execute_async(
|
|
1996
|
+
method,
|
|
1997
|
+
url,
|
|
1998
|
+
headers=headers,
|
|
1999
|
+
header_order=header_order,
|
|
2000
|
+
body=body,
|
|
2001
|
+
_stream_output_path=c_path,
|
|
2002
|
+
_stream_output_block_size=chunk_size,
|
|
2003
|
+
_stream_output_eof_symbol=c_eof,
|
|
2004
|
+
_stream_keep_alive=keep_alive,
|
|
2005
|
+
**kwargs,
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
async def execute_request(self, method: str, url: str, **kwargs: Any) -> Response:
|
|
2009
|
+
return await self._execute_async(method, url, **kwargs)
|
|
2010
|
+
|
|
2011
|
+
async def get(self, url: str, **kwargs: Any) -> Response:
|
|
2012
|
+
return await self._execute_async("GET", url, **kwargs)
|
|
2013
|
+
|
|
2014
|
+
async def post(self, url: str, **kwargs: Any) -> Response:
|
|
2015
|
+
return await self._execute_async("POST", url, **kwargs)
|
|
2016
|
+
|
|
2017
|
+
async def head(self, url: str, **kwargs: Any) -> Response:
|
|
2018
|
+
return await self._execute_async("HEAD", url, **kwargs)
|
|
2019
|
+
|
|
2020
|
+
async def put(self, url: str, **kwargs: Any) -> Response:
|
|
2021
|
+
return await self._execute_async("PUT", url, **kwargs)
|
|
2022
|
+
|
|
2023
|
+
async def delete(self, url: str, **kwargs: Any) -> Response:
|
|
2024
|
+
return await self._execute_async("DELETE", url, **kwargs)
|
|
2025
|
+
|
|
2026
|
+
async def patch(self, url: str, **kwargs: Any) -> Response:
|
|
2027
|
+
return await self._execute_async("PATCH", url, **kwargs)
|
|
2028
|
+
|
|
2029
|
+
@staticmethod
|
|
2030
|
+
async def clear_client_pool() -> None:
|
|
2031
|
+
"""Close all idle connections in the global Go client pool.
|
|
2032
|
+
|
|
2033
|
+
Calls the Go C function directly. ClearClientPool iterates the
|
|
2034
|
+
internal client map and closes idle connections — it is fast and
|
|
2035
|
+
non-blocking, so no thread pool is needed (Constraint 4).
|
|
2036
|
+
"""
|
|
2037
|
+
_, lib = _get_ffi()
|
|
2038
|
+
lib.ClearClientPool()
|
|
2039
|
+
# Yield to the event loop to be cooperative
|
|
2040
|
+
await asyncio.sleep(0)
|
|
2041
|
+
|
|
2042
|
+
|
|
2043
|
+
# Convenience top-level function
|
|
2044
|
+
def clear_client_pool() -> None:
|
|
2045
|
+
"""Close all idle connections in the global Go client pool (synchronous)."""
|
|
2046
|
+
Session.clear_client_pool()
|