bas-http 1.0.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.
bas/_curl.py ADDED
@@ -0,0 +1,393 @@
1
+ """
2
+ Low-level ctypes bindings for libcurl with curl-impersonate support.
3
+
4
+ This module provides Python bindings to libcurl (or curl-impersonate if available)
5
+ using ctypes. It avoids cffi compilation issues that plague curlcffi.
6
+ """
7
+
8
+ import ctypes
9
+ import ctypes.util
10
+ import os
11
+ import sys
12
+ from ctypes import (
13
+ CFUNCTYPE,
14
+ POINTER,
15
+ c_char,
16
+ c_char_p,
17
+ c_int,
18
+ c_long,
19
+ c_off_t,
20
+ c_void_p,
21
+ c_slist,
22
+ )
23
+ from typing import Optional
24
+
25
+ # ── libcurl loading ──────────────────────────────────────────────────────────
26
+ # Priority: curl-impersonate first, then system libcurl.
27
+
28
+ _CURL_LIB_NAMES = [
29
+ # curl-impersonate shared libraries
30
+ "curl-impersonate-chrome",
31
+ "curl-impersonate-firefox",
32
+ "libcurl-impersonate-chrome",
33
+ "libcurl-impersonate-firefox",
34
+ # Standard libcurl
35
+ "curl",
36
+ "libcurl",
37
+ "libcurl.so.4",
38
+ "libcurl.4.dylib",
39
+ "curl.dll",
40
+ ]
41
+
42
+
43
+ def _find_curl_library() -> ctypes.CDLL:
44
+ """Locate and load the best available curl library."""
45
+ # 1. Check FREEBUFF_CURL_PATH env var
46
+ env_path = os.environ.get("FREEBUFF_CURL_PATH")
47
+ if env_path and os.path.isfile(env_path):
48
+ return ctypes.CDLL(env_path)
49
+
50
+ # 2. Check system paths
51
+ for name in _CURL_LIB_NAMES:
52
+ found = ctypes.util.find_library(name)
53
+ if found:
54
+ try:
55
+ return ctypes.CDLL(found)
56
+ except OSError:
57
+ continue
58
+
59
+ # 3. Try direct load on common paths
60
+ if sys.platform == "linux":
61
+ for path in [
62
+ "/usr/lib/x86_64-linux-gnu/libcurl.so.4",
63
+ "/usr/lib/libcurl.so.4",
64
+ "/usr/local/lib/libcurl.so.4",
65
+ ]:
66
+ if os.path.isfile(path):
67
+ return ctypes.CDLL(path)
68
+ elif sys.platform == "darwin":
69
+ for path in [
70
+ "/usr/local/lib/libcurl.4.dylib",
71
+ "/opt/homebrew/lib/libcurl.4.dylib",
72
+ ]:
73
+ if os.path.isfile(path):
74
+ return ctypes.CDLL(path)
75
+
76
+ raise OSError(
77
+ "Could not find libcurl. Install libcurl-dev (Linux) or "
78
+ "curl-impersonate (for browser impersonation). "
79
+ "Set FREEBUFF_CURL_PATH to a custom library path."
80
+ )
81
+
82
+
83
+ _lib = _find_curl_library()
84
+
85
+
86
+ # ── Constants ────────────────────────────────────────────────────────────────
87
+
88
+ # CURLoptions
89
+ CURL_GLOBAL_ALL = 3
90
+ CURLOPT_URL = 10002
91
+ CURLOPT_WRITEFUNCTION = 20011
92
+ CURLOPT_WRITEDATA = 10001
93
+ CURLOPT_HEADERFUNCTION = 20079
94
+ CURLOPT_HEADERDATA = 10029
95
+ CURLOPT_POSTFIELDS = 10015
96
+ CURLOPT_POST = 47
97
+ CURLOPT_CUSTOMREQUEST = 10036
98
+ CURLOPT_FOLLOWLOCATION = 52
99
+ CURLOPT_MAXREDIRS = 68
100
+ CURLOPT_COOKIEFILE = 10031
101
+ CURLOPT_COOKIEJAR = 10082
102
+ CURLOPT_COOKIELIST = 10135
103
+ CURLOPT_COOKIE = 10022
104
+ CURLOPT_HTTPHEADER = 10023
105
+ CURLOPT_HTTPGET = 80
106
+ CURLOPT_SSL_VERIFYPEER = 64
107
+ CURLOPT_SSL_VERIFYHOST = 81
108
+ CURLOPT_CAINFO = 10065
109
+ CURLOPT_TIMEOUT = 13
110
+ CURLOPT_CONNECTTIMEOUT = 78
111
+ CURLOPT_USERAGENT = 10018
112
+ CURLOPT_REFERER = 10016
113
+ CURLOPT_ENCODING = 10106
114
+ CURLOPT_PROXY = 10004
115
+ CURLOPT_PROXYPORT = 59
116
+ CURLOPT_PROXYTYPE = 101
117
+ CURLOPT_NOPROXY = 10177
118
+ CURLOPT_RESOLVE = 10203
119
+ CURLOPT_INTERFACE = 10062
120
+ CURLOPT_LOCALPORT = 139
121
+ CURLOPT_DNS_CACHE_TIMEOUT = 92
122
+ CURLOPT_TCP_KEEPALIVE = 213
123
+ CURLOPT_TCP_KEEPIDLE = 214
124
+ CURLOPT_TCP_KEEPINTVL = 215
125
+ CURLOPT_SSL_FALSESTART = 224
126
+ CURLOPT_TCP_FASTOPEN = 244
127
+ CURLOPT_HTTP_VERSION = 84
128
+ CURLOPT_CERTINFO = 172
129
+ CURLOPT_PINNEDPUBLICKEY = 10230
130
+ CURLOPT_NO_SIGNAL = 99
131
+ CURLOPT_TRANSFER_ENCODING = 20206
132
+ CURLOPT_HTTP200ALIASES = 10260
133
+ CURLOPT_PREREQFUNCTION = 20328
134
+ CURLOPT_PREREQDATA = 10313
135
+ CURLOPT_MAIL_FROM = 10186
136
+ CURLOPT_MAIL_RCPT = 10187
137
+ CURLOPT_UPLOAD = 46
138
+ CURLOPT_INFILESIZE = 14
139
+ CURLOPT_INFILESIZE_LARGE = 10311
140
+ CURLOPT_UPLOAD = 46
141
+ CURLOPT_PORT = 3
142
+
143
+ # CURLinfo
144
+ CURLINFO_RESPONSE_CODE = 0x200002
145
+ CURLINFO_EFFECTIVE_URL = 0x100002
146
+ CURLINFO_CONTENT_TYPE = 0x100004
147
+ CURLINFO_REDIRECT_URL = 0x100007
148
+ CURLINFO_HEADER_SIZE = 0x20000B
149
+ CURLINFO_TOTAL_TIME = 0x300003
150
+ CURLINFO_NAMELOOKUP_TIME = 0x300004
151
+ CURLINFO_CONNECT_TIME = 0x300005
152
+ CURLINFO_PRETRANSFER_TIME = 0x300006
153
+ CURLINFO_STARTTRANSFER_TIME = 0x300009
154
+ CURLINFO_PRIMARY_IP = 0x40000C
155
+ CURLINFO_PRIMARY_PORT = 0x40000F
156
+ CURLINFO_SSL_VERIFYRESULT = 0x400008
157
+ CURLINFO_HTTP_VERSION = 0x400012
158
+
159
+ # CURLcode
160
+ CURL_OK = 0
161
+
162
+ # CURLMcode
163
+ CURLM_OK = 0
164
+
165
+ # CURLHTTPVersion
166
+ CURL_HTTP_VERSION_NONE = 0
167
+ CURL_HTTP_VERSION_1_0 = 1
168
+ CURL_HTTP_VERSION_1_1 = 2
169
+ CURL_HTTP_VERSION_2_0 = 3
170
+ CURL_HTTP_VERSION_2TLS = 4
171
+ CURL_HTTP_VERSION_3 = 30
172
+
173
+ # CURLProxyType
174
+ CURLPROXY_HTTP = 0
175
+ CURLPROXY_SOCKS4 = 4
176
+ CURLPROXY_SOCKS5 = 5
177
+ CURLPROXY_SOCKS4A = 6
178
+ CURLPROXY_SOCKS5_HOSTNAME = 7
179
+
180
+
181
+ # ── CURLINFO enum for curl_slist operations ──────────────────────────────────
182
+
183
+ # CURLOPT_COOKIELIST action
184
+ CURLCOOKIE_SET = 1
185
+ CURLCOOKIE_UNSET = 3
186
+
187
+
188
+ # ── Function signatures ──────────────────────────────────────────────────────
189
+
190
+ _lib.curl_global_init.argtypes = [c_long]
191
+ _lib.curl_global_init.restype = c_int
192
+
193
+ _lib.curl_easy_init.argtypes = []
194
+ _lib.curl_easy_init.restype = c_void_p
195
+
196
+ _lib.curl_easy_cleanup.argtypes = [c_void_p]
197
+ _lib.curl_easy_cleanup.restype = None
198
+
199
+ _lib.curl_easy_setopt.argtypes = [c_void_p, c_int, ...]
200
+ _lib.curl_easy_setopt.restype = c_int
201
+
202
+ _lib.curl_easy_perform.argtypes = [c_void_p]
203
+ _lib.curl_easy_perform.restype = c_int
204
+
205
+ _lib.curl_easy_getinfo.argtypes = [c_void_p, c_int, c_void_p]
206
+ _lib.curl_easy_getinfo.restype = c_int
207
+
208
+ _lib.curl_easy_reset.argtypes = [c_void_p]
209
+ _lib.curl_easy_reset.restype = None
210
+
211
+ _lib.curl_easy_duphandle.argtypes = [c_void_p]
212
+ _lib.curl_easy_duphandle.restype = c_void_p
213
+
214
+ _lib.curl_slist_append.argtypes = [POINTER(c_slist), c_char_p]
215
+ _lib.curl_slist_append.restype = POINTER(c_slist)
216
+
217
+ _lib.curl_slist_free_all.argtypes = [POINTER(c_slist)]
218
+ _lib.curl_slist_free_all.restype = None
219
+
220
+ _lib.curl_free.argtypes = [c_void_p]
221
+ _lib.curl_free.restype = None
222
+
223
+ _lib.curl_easy_strerror.argtypes = [c_int]
224
+ _lib.curl_easy_strerror.restype = c_char_p
225
+
226
+ _lib.curl_version.argtypes = []
227
+ _lib.curl_version.restype = c_char_p
228
+
229
+
230
+ # ── Callback types ───────────────────────────────────────────────────────────
231
+
232
+ WRITEFUNC = CFUNCTYPE(c_int, c_char_p, c_int, c_int, c_void_p)
233
+ HEADERFUNC = CFUNCTYPE(c_int, c_char_p, c_int, c_int, c_void_p)
234
+
235
+
236
+ # ── Global init ──────────────────────────────────────────────────────────────
237
+
238
+ _lib.curl_global_init(CURL_GLOBAL_ALL)
239
+
240
+
241
+ # ── Helper classes ───────────────────────────────────────────────────────────
242
+
243
+ class CurlSList:
244
+ """Wrapper around curl_slist* for safe memory management."""
245
+
246
+ def __init__(self):
247
+ self._list = POINTER(c_slist)()
248
+ self._items: list[bytes] = []
249
+
250
+ def append(self, item: str) -> None:
251
+ encoded = item.encode("utf-8") if isinstance(item, str) else item
252
+ self._list = _lib.curl_slist_append(self._list, encoded)
253
+ self._items.append(encoded)
254
+
255
+ def __del__(self):
256
+ if self._list:
257
+ _lib.curl_slist_free_all(self._list)
258
+
259
+ def as_cpointer(self) -> POINTER(c_slist):
260
+ return self._list
261
+
262
+
263
+ # ── Easy handle wrapper ─────────────────────────────────────────────────────
264
+
265
+ class CurlEasy:
266
+ """
267
+ Wrapper around a CURL* easy handle.
268
+
269
+ Provides a Pythonic interface while keeping all the power of libcurl.
270
+ """
271
+
272
+ def __init__(self):
273
+ self._handle = _lib.curl_easy_init()
274
+ if not self._handle:
275
+ raise RuntimeError("Failed to initialize curl easy handle")
276
+
277
+ self._headers_list: Optional[CurlSList] = None
278
+ self._write_callback: Optional[WRITEFUNC] = None
279
+ self._header_callback: Optional[HEADERFUNC] = None
280
+ self._write_buffer = bytearray()
281
+ self._header_buffer = bytearray()
282
+
283
+ # Default settings
284
+ self.setopt(CURLOPT_FOLLOWLOCATION, 1)
285
+ self.setopt(CURLOPT_MAXREDIRS, 20)
286
+ self.setopt(CURLOPT_SSL_VERIFYPEER, 1)
287
+ self.setopt(CURLOPT_SSL_VERIFYHOST, 2)
288
+ self.setopt(CURLOPT_TIMEOUT, 30)
289
+ self.setopt(CURLOPT_CONNECTTIMEOUT, 10)
290
+ self.setopt(CURLOPT_DNS_CACHE_TIMEOUT, 300)
291
+ self.setopt(CURLOPT_TCP_KEEPALIVE, 1)
292
+ self.setopt(CURLOPT_NO_SIGNAL, 1)
293
+ self.setopt(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS)
294
+
295
+ def setopt(self, option: int, value) -> None:
296
+ """Set a curl option."""
297
+ if isinstance(value, int):
298
+ _lib.curl_easy_setopt(self._handle, option, c_long(value))
299
+ elif isinstance(value, str):
300
+ _lib.curl_easy_setopt(self._handle, option, value.encode("utf-8"))
301
+ elif isinstance(value, bytes):
302
+ _lib.curl_easy_setopt(self._handle, option, value)
303
+ elif isinstance(value, CurlSList):
304
+ _lib.curl_easy_setopt(self._handle, option, value.as_cpointer())
305
+ else:
306
+ raise TypeError(f"Unsupported option type: {type(value)}")
307
+
308
+ def perform(self) -> int:
309
+ """Execute the curl request."""
310
+ return _lib.curl_easy_perform(self._handle)
311
+
312
+ def getinfo(self, info: int):
313
+ """Get information about the last performed request."""
314
+ if info in (CURLINFO_RESPONSE_CODE, CURLINFO_HEADER_SIZE, CURLINFO_HTTP_VERSION):
315
+ val = c_long()
316
+ _lib.curl_easy_getinfo(self._handle, info, ctypes.byref(val))
317
+ return val.value
318
+ elif info in (
319
+ CURLINFO_EFFECTIVE_URL,
320
+ CURLINFO_CONTENT_TYPE,
321
+ CURLINFO_REDIRECT_URL,
322
+ CURLINFO_PRIMARY_IP,
323
+ ):
324
+ val = c_char_p()
325
+ _lib.curl_easy_getinfo(self._handle, info, ctypes.byref(val))
326
+ return val.value.decode("utf-8") if val.value else None
327
+ elif info == CURLINFO_TOTAL_TIME:
328
+ val = c_double()
329
+ _lib.curl_easy_getinfo(self._handle, info, ctypes.byref(val))
330
+ return val.value
331
+ elif info == CURLINFO_SSL_VERIFYRESULT:
332
+ val = c_int()
333
+ _lib.curl_easy_getinfo(self._handle, info, ctypes.byref(val))
334
+ return val.value
335
+ raise ValueError(f"Unsupported info: {info}")
336
+
337
+ def set_headers(self, headers: Optional[dict[str, str]]) -> None:
338
+ """Set request headers."""
339
+ if self._headers_list:
340
+ _lib.curl_slist_free_all(self._headers_list)
341
+ self._headers_list = None
342
+
343
+ if headers:
344
+ self._headers_list = CurlSList()
345
+ for key, value in headers.items():
346
+ self._headers_list.append(f"{key}: {value}")
347
+ self.setopt(CURLOPT_HTTPHEADER, self._headers_list)
348
+
349
+ def reset(self) -> None:
350
+ """Reset the handle to default state."""
351
+ _lib.curl_easy_reset(self._handle)
352
+ # Re-apply defaults
353
+ self.setopt(CURLOPT_FOLLOWLOCATION, 1)
354
+ self.setopt(CURLOPT_MAXREDIRS, 20)
355
+ self.setopt(CURLOPT_SSL_VERIFYPEER, 1)
356
+ self.setopt(CURLOPT_SSL_VERIFYHOST, 2)
357
+ self.setopt(CURLOPT_TIMEOUT, 30)
358
+ self.setopt(CURLOPT_CONNECTTIMEOUT, 10)
359
+ self.setopt(CURLOPT_DNS_CACHE_TIMEOUT, 300)
360
+ self.setopt(CURLOPT_TCP_KEEPALIVE, 1)
361
+ self.setopt(CURLOPT_NO_SIGNAL, 1)
362
+ self.setopt(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS)
363
+
364
+ def duphandle(self) -> "CurlEasy":
365
+ """Duplicate this handle (for session forking)."""
366
+ new = CurlEasy.__new__(CurlEasy)
367
+ new._handle = _lib.curl_easy_duphandle(self._handle)
368
+ if not new._handle:
369
+ raise RuntimeError("Failed to duplicate curl handle")
370
+ new._headers_list = None
371
+ new._write_callback = None
372
+ new._header_callback = None
373
+ new._write_buffer = bytearray()
374
+ new._header_buffer = bytearray()
375
+ return new
376
+
377
+ def strerror(self, code: int) -> str:
378
+ """Get human-readable error string."""
379
+ msg = _lib.curl_easy_strerror(code)
380
+ return msg.decode("utf-8") if msg else f"Unknown error {code}"
381
+
382
+ def __del__(self):
383
+ if hasattr(self, "_handle") and self._handle:
384
+ if self._headers_list:
385
+ _lib.curl_slist_free_all(self._headers_list)
386
+ _lib.curl_easy_cleanup(self._handle)
387
+ self._handle = None
388
+
389
+
390
+ def curl_version() -> str:
391
+ """Return the libcurl version string."""
392
+ v = _lib.curl_version()
393
+ return v.decode("utf-8") if v else "unknown"