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/_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()