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/README.md +244 -0
- bas/__init__.py +97 -0
- bas/_curl.py +393 -0
- bas/async_session.py +352 -0
- bas/cookies.py +643 -0
- bas/curl_parser.py +267 -0
- bas/impersonate.py +424 -0
- bas/models.py +339 -0
- bas/session.py +648 -0
- bas/simple_session.py +299 -0
- bas/tests/__init__.py +0 -0
- bas/tests/test_cookies.py +778 -0
- bas/tests/test_curl_parser.py +172 -0
- bas/tests/test_real_request.py +142 -0
- bas/tests/test_spaceshooter.py +213 -0
- bas_http-1.0.0.dist-info/METADATA +25 -0
- bas_http-1.0.0.dist-info/RECORD +19 -0
- bas_http-1.0.0.dist-info/WHEEL +5 -0
- bas_http-1.0.0.dist-info/top_level.txt +1 -0
bas/curl_parser.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse curl commands into bas requests.
|
|
3
|
+
|
|
4
|
+
The workflow:
|
|
5
|
+
1. Open browser DevTools -> Network tab
|
|
6
|
+
2. Right-click a request -> "Copy as cURL"
|
|
7
|
+
3. Paste into bas
|
|
8
|
+
|
|
9
|
+
bas parses it and gives you a ready-to-go Session with all
|
|
10
|
+
headers, cookies, and settings extracted.
|
|
11
|
+
|
|
12
|
+
Handles both Windows (^") and Unix (\') escaping, and escaped quotes
|
|
13
|
+
inside header values like sec-ch-ua.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import Optional
|
|
18
|
+
from .cookies import Cookie, CookieJar
|
|
19
|
+
from .models import Headers
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _tokenize_curl(cmd: str) -> list[str]:
|
|
23
|
+
"""
|
|
24
|
+
Tokenize a curl command string, handling:
|
|
25
|
+
- Double-quoted strings with escaped quotes inside (\")
|
|
26
|
+
- Single-quoted strings
|
|
27
|
+
- Unquoted tokens
|
|
28
|
+
- Windows ^" escaping
|
|
29
|
+
- Windows ^ line continuations
|
|
30
|
+
"""
|
|
31
|
+
# Normalize Windows line continuations
|
|
32
|
+
cmd = re.sub(r'\s*\^\s*\n', ' ', cmd)
|
|
33
|
+
# Windows caret-quote: ^" -> "
|
|
34
|
+
cmd = cmd.replace('^"', '"').replace('^"', '"')
|
|
35
|
+
# Windows ^$ -> $
|
|
36
|
+
cmd = cmd.replace('^$', '$')
|
|
37
|
+
|
|
38
|
+
tokens = []
|
|
39
|
+
i = 0
|
|
40
|
+
while i < len(cmd):
|
|
41
|
+
# Skip whitespace
|
|
42
|
+
if cmd[i].isspace():
|
|
43
|
+
i += 1
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if cmd[i] == '"':
|
|
47
|
+
# Double-quoted string — handle escaped quotes \"
|
|
48
|
+
i += 1 # skip opening quote
|
|
49
|
+
buf = []
|
|
50
|
+
while i < len(cmd):
|
|
51
|
+
if cmd[i] == '\\' and i + 1 < len(cmd) and cmd[i + 1] == '"':
|
|
52
|
+
buf.append('"')
|
|
53
|
+
i += 2
|
|
54
|
+
elif cmd[i] == '"':
|
|
55
|
+
i += 1 # skip closing quote
|
|
56
|
+
break
|
|
57
|
+
else:
|
|
58
|
+
buf.append(cmd[i])
|
|
59
|
+
i += 1
|
|
60
|
+
tokens.append(''.join(buf))
|
|
61
|
+
|
|
62
|
+
elif cmd[i] == "'":
|
|
63
|
+
# Single-quoted string — no escaping
|
|
64
|
+
i += 1
|
|
65
|
+
buf = []
|
|
66
|
+
while i < len(cmd):
|
|
67
|
+
if cmd[i] == "'":
|
|
68
|
+
i += 1
|
|
69
|
+
break
|
|
70
|
+
else:
|
|
71
|
+
buf.append(cmd[i])
|
|
72
|
+
i += 1
|
|
73
|
+
tokens.append(''.join(buf))
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
# Unquoted token
|
|
77
|
+
buf = []
|
|
78
|
+
while i < len(cmd) and not cmd[i].isspace():
|
|
79
|
+
buf.append(cmd[i])
|
|
80
|
+
i += 1
|
|
81
|
+
tokens.append(''.join(buf))
|
|
82
|
+
|
|
83
|
+
return tokens
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_curl(curl_cmd: str) -> dict:
|
|
87
|
+
"""
|
|
88
|
+
Parse a curl command string into its components.
|
|
89
|
+
|
|
90
|
+
Handles Windows (^"), Unix (\'), and escaped quotes inside values.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
{
|
|
94
|
+
"method": "GET" | "POST" | "PUT" | ...,
|
|
95
|
+
"url": "https://...",
|
|
96
|
+
"headers": {"key": "value", ...},
|
|
97
|
+
"cookies": {"name": "value", ...},
|
|
98
|
+
"cookie_header": "raw cookie string",
|
|
99
|
+
"data": "post body" or None,
|
|
100
|
+
"user_agent": "...",
|
|
101
|
+
"referer": "...",
|
|
102
|
+
"origin": "...",
|
|
103
|
+
}
|
|
104
|
+
"""
|
|
105
|
+
result = {
|
|
106
|
+
"method": "GET",
|
|
107
|
+
"url": "",
|
|
108
|
+
"headers": {},
|
|
109
|
+
"cookies": {},
|
|
110
|
+
"cookie_header": "",
|
|
111
|
+
"data": None,
|
|
112
|
+
"user_agent": "",
|
|
113
|
+
"referer": "",
|
|
114
|
+
"origin": "",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
tokens = _tokenize_curl(curl_cmd)
|
|
118
|
+
|
|
119
|
+
# Skip "curl" token
|
|
120
|
+
start = 0
|
|
121
|
+
if tokens and tokens[0].lower() == "curl":
|
|
122
|
+
start = 1
|
|
123
|
+
|
|
124
|
+
# First non-flag token is the URL
|
|
125
|
+
i = start
|
|
126
|
+
while i < len(tokens):
|
|
127
|
+
if tokens[i].startswith("-"):
|
|
128
|
+
# It's a flag, skip for now to find URL
|
|
129
|
+
if tokens[i] in ("-H", "-b", "-d", "-X", "-A", "-e", "--data"):
|
|
130
|
+
i += 2 # skip flag + value
|
|
131
|
+
else:
|
|
132
|
+
i += 1
|
|
133
|
+
else:
|
|
134
|
+
# It's the URL
|
|
135
|
+
result["url"] = tokens[i]
|
|
136
|
+
i += 1
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
# Now parse flags
|
|
140
|
+
while i < len(tokens):
|
|
141
|
+
token = tokens[i]
|
|
142
|
+
|
|
143
|
+
if token == "-H" and i + 1 < len(tokens):
|
|
144
|
+
val = tokens[i + 1]
|
|
145
|
+
if ":" in val:
|
|
146
|
+
key, _, value = val.partition(":")
|
|
147
|
+
key = key.strip()
|
|
148
|
+
value = value.strip()
|
|
149
|
+
result["headers"][key] = value
|
|
150
|
+
|
|
151
|
+
lk = key.lower()
|
|
152
|
+
if lk == "user-agent":
|
|
153
|
+
result["user_agent"] = value
|
|
154
|
+
elif lk == "referer":
|
|
155
|
+
result["referer"] = value
|
|
156
|
+
elif lk == "origin":
|
|
157
|
+
result["origin"] = value
|
|
158
|
+
i += 2
|
|
159
|
+
|
|
160
|
+
elif token in ("-b", "--cookie") and i + 1 < len(tokens):
|
|
161
|
+
cookie_str = tokens[i + 1]
|
|
162
|
+
result["cookie_header"] = cookie_str
|
|
163
|
+
for pair in cookie_str.split(";"):
|
|
164
|
+
pair = pair.strip()
|
|
165
|
+
if "=" in pair:
|
|
166
|
+
name, _, value = pair.partition("=")
|
|
167
|
+
name = name.strip()
|
|
168
|
+
value = value.strip()
|
|
169
|
+
if name:
|
|
170
|
+
result["cookies"][name] = value
|
|
171
|
+
i += 2
|
|
172
|
+
|
|
173
|
+
elif token in ("-d", "--data", "--data-raw") and i + 1 < len(tokens):
|
|
174
|
+
result["data"] = tokens[i + 1]
|
|
175
|
+
if "content-type" not in {k.lower(): v for k, v in result["headers"].items()}:
|
|
176
|
+
result["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
|
177
|
+
i += 2
|
|
178
|
+
|
|
179
|
+
elif token == "-X" and i + 1 < len(tokens):
|
|
180
|
+
result["method"] = tokens[i + 1].upper()
|
|
181
|
+
i += 2
|
|
182
|
+
|
|
183
|
+
elif token in ("-A", "--user-agent") and i + 1 < len(tokens):
|
|
184
|
+
result["headers"]["User-Agent"] = tokens[i + 1]
|
|
185
|
+
result["user_agent"] = tokens[i + 1]
|
|
186
|
+
i += 2
|
|
187
|
+
|
|
188
|
+
elif token in ("-e", "--referer") and i + 1 < len(tokens):
|
|
189
|
+
result["headers"]["Referer"] = tokens[i + 1]
|
|
190
|
+
result["referer"] = tokens[i + 1]
|
|
191
|
+
i += 2
|
|
192
|
+
|
|
193
|
+
elif token == "--compressed":
|
|
194
|
+
i += 1
|
|
195
|
+
|
|
196
|
+
elif token == "-k":
|
|
197
|
+
i += 1
|
|
198
|
+
|
|
199
|
+
elif token.startswith("-"):
|
|
200
|
+
# Unknown flag, skip it and its value if it looks like it takes one
|
|
201
|
+
i += 1
|
|
202
|
+
if i < len(tokens) and not tokens[i].startswith("-"):
|
|
203
|
+
i += 1
|
|
204
|
+
else:
|
|
205
|
+
i += 1
|
|
206
|
+
|
|
207
|
+
if result["data"] and result["method"] == "GET":
|
|
208
|
+
result["method"] = "POST"
|
|
209
|
+
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def curl_to_session(curl_cmd: str, **kwargs):
|
|
214
|
+
"""
|
|
215
|
+
Parse a curl command and return a pre-configured Session.
|
|
216
|
+
|
|
217
|
+
Usage:
|
|
218
|
+
>>> s = curl_to_session('''curl "https://example.com" -H "User-Agent: ..." -b "cookie=value"''')
|
|
219
|
+
>>> r = s.get("https://example.com/other-page")
|
|
220
|
+
"""
|
|
221
|
+
from .simple_session import Session
|
|
222
|
+
|
|
223
|
+
parsed = parse_curl(curl_cmd)
|
|
224
|
+
|
|
225
|
+
s = Session(**kwargs)
|
|
226
|
+
s.headers.update(parsed["headers"])
|
|
227
|
+
|
|
228
|
+
url = parsed["url"]
|
|
229
|
+
if url:
|
|
230
|
+
from urllib.parse import urlparse
|
|
231
|
+
domain = urlparse(url).hostname or ""
|
|
232
|
+
for name, value in parsed["cookies"].items():
|
|
233
|
+
s.set_cookie(name, value, domain=domain, path="/")
|
|
234
|
+
|
|
235
|
+
return s
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def print_curl_summary(curl_cmd: str) -> None:
|
|
239
|
+
"""Print a human-readable summary of a curl command."""
|
|
240
|
+
parsed = parse_curl(curl_cmd)
|
|
241
|
+
|
|
242
|
+
print("=" * 60)
|
|
243
|
+
print("CURL COMMAND SUMMARY")
|
|
244
|
+
print("=" * 60)
|
|
245
|
+
print(f" Method: {parsed['method']}")
|
|
246
|
+
print(f" URL: {parsed['url']}")
|
|
247
|
+
print(f" Headers: {len(parsed['headers'])}")
|
|
248
|
+
print(f" Cookies: {len(parsed['cookies'])}")
|
|
249
|
+
|
|
250
|
+
if parsed["data"]:
|
|
251
|
+
data_preview = parsed["data"][:80] + ("..." if len(parsed["data"]) > 80 else "")
|
|
252
|
+
print(f" Body: {data_preview}")
|
|
253
|
+
|
|
254
|
+
print()
|
|
255
|
+
print("Headers:")
|
|
256
|
+
for key, value in parsed["headers"].items():
|
|
257
|
+
preview = value[:60] + ("..." if len(value) > 60 else "")
|
|
258
|
+
print(f" {key}: {preview}")
|
|
259
|
+
|
|
260
|
+
if parsed["cookies"]:
|
|
261
|
+
print()
|
|
262
|
+
print("Cookies:")
|
|
263
|
+
for name, value in parsed["cookies"].items():
|
|
264
|
+
preview = value[:40] + ("..." if len(value) > 40 else "")
|
|
265
|
+
print(f" {name} = {preview}")
|
|
266
|
+
|
|
267
|
+
print("=" * 60)
|
bas/impersonate.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Browser impersonation profiles.
|
|
3
|
+
|
|
4
|
+
This module provides browser fingerprint profiles that override HTTP/2
|
|
5
|
+
settings and headers to mimic real browsers. When curl-impersonate is
|
|
6
|
+
available, it also enables TLS fingerprint mimicry.
|
|
7
|
+
|
|
8
|
+
Unlike curlcffi which bundles impersonation in a single enum, bas
|
|
9
|
+
separates the concerns:
|
|
10
|
+
- TLS impersonation (requires curl-impersonate library)
|
|
11
|
+
- HTTP/2 fingerprinting (works with standard libcurl)
|
|
12
|
+
- Header ordering and values (always works)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BrowserProfile:
|
|
19
|
+
"""
|
|
20
|
+
Defines how to mimic a specific browser.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
name: Human-readable browser name
|
|
24
|
+
h2_settings: HTTP/2 SETTINGS frame parameters
|
|
25
|
+
h2_weight: HTTP/2 priority stream weight
|
|
26
|
+
h2_depends_on: HTTP/2 priority stream dependency
|
|
27
|
+
h2_exclusive: HTTP/2 priority stream exclusive flag
|
|
28
|
+
tls_version: Minimum TLS version to advertise
|
|
29
|
+
http_headers: Headers to send (in order)
|
|
30
|
+
pseudo_headers: HTTP/2 pseudo-headers order
|
|
31
|
+
header_order: Header order (True = use defined order)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
name: str,
|
|
37
|
+
h2_settings: Optional[dict] = None,
|
|
38
|
+
h2_weight: int = 1,
|
|
39
|
+
h2_depends_on: int = 0,
|
|
40
|
+
h2_exclusive: bool = False,
|
|
41
|
+
tls_version: str = "1.3",
|
|
42
|
+
http_headers: Optional[dict] = None,
|
|
43
|
+
pseudo_headers: Optional[list] = None,
|
|
44
|
+
header_order: bool = True,
|
|
45
|
+
curl_impersonate: Optional[str] = None,
|
|
46
|
+
):
|
|
47
|
+
self.name = name
|
|
48
|
+
self.h2_settings = h2_settings or {}
|
|
49
|
+
self.h2_weight = h2_weight
|
|
50
|
+
self.h2_depends_on = h2_depends_on
|
|
51
|
+
self.h2_exclusive = h2_exclusive
|
|
52
|
+
self.tls_version = tls_version
|
|
53
|
+
self.http_headers = http_headers or {}
|
|
54
|
+
self.pseudo_headers = pseudo_headers or []
|
|
55
|
+
self.header_order = header_order
|
|
56
|
+
self.curl_impersonate = curl_impersonate # curl-impersonate profile name
|
|
57
|
+
|
|
58
|
+
def __repr__(self):
|
|
59
|
+
return f"BrowserProfile({self.name!r})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Pre-defined browser profiles ─────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
CHROME131 = BrowserProfile(
|
|
65
|
+
name="chrome131",
|
|
66
|
+
curl_impersonate="chrome131",
|
|
67
|
+
h2_settings={
|
|
68
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
69
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
70
|
+
"INITIAL_WINDOW_SIZE": 6291456,
|
|
71
|
+
"MAX_FRAME_SIZE": 16384,
|
|
72
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
73
|
+
},
|
|
74
|
+
h2_weight=256,
|
|
75
|
+
http_headers={
|
|
76
|
+
"User-Agent": (
|
|
77
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
78
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
79
|
+
"Chrome/131.0.0.0 Safari/537.36"
|
|
80
|
+
),
|
|
81
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
82
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
83
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
84
|
+
"Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
|
|
85
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
86
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
87
|
+
"Sec-Fetch-Dest": "document",
|
|
88
|
+
"Sec-Fetch-Mode": "navigate",
|
|
89
|
+
"Sec-Fetch-Site": "none",
|
|
90
|
+
"Sec-Fetch-User": "?1",
|
|
91
|
+
"Upgrade-Insecure-Requests": "1",
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
CHROME124 = BrowserProfile(
|
|
96
|
+
name="chrome124",
|
|
97
|
+
curl_impersonate="chrome124",
|
|
98
|
+
h2_settings={
|
|
99
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
100
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
101
|
+
"INITIAL_WINDOW_SIZE": 6291456,
|
|
102
|
+
"MAX_FRAME_SIZE": 16384,
|
|
103
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
104
|
+
},
|
|
105
|
+
http_headers={
|
|
106
|
+
"User-Agent": (
|
|
107
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
108
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
109
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
110
|
+
),
|
|
111
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
112
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
113
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
114
|
+
"Sec-Ch-Ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
|
|
115
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
116
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
117
|
+
"Sec-Fetch-Dest": "document",
|
|
118
|
+
"Sec-Fetch-Mode": "navigate",
|
|
119
|
+
"Sec-Fetch-Site": "none",
|
|
120
|
+
"Sec-Fetch-User": "?1",
|
|
121
|
+
"Upgrade-Insecure-Requests": "1",
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
CHROME120 = BrowserProfile(
|
|
126
|
+
name="chrome120",
|
|
127
|
+
curl_impersonate="chrome120",
|
|
128
|
+
h2_settings={
|
|
129
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
130
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
131
|
+
"INITIAL_WINDOW_SIZE": 6291456,
|
|
132
|
+
"MAX_FRAME_SIZE": 16384,
|
|
133
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
134
|
+
},
|
|
135
|
+
http_headers={
|
|
136
|
+
"User-Agent": (
|
|
137
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
138
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
139
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
140
|
+
),
|
|
141
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
142
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
143
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
144
|
+
"Sec-Ch-Ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
145
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
146
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
147
|
+
"Sec-Fetch-Dest": "document",
|
|
148
|
+
"Sec-Fetch-Mode": "navigate",
|
|
149
|
+
"Sec-Fetch-Site": "none",
|
|
150
|
+
"Sec-Fetch-User": "?1",
|
|
151
|
+
"Upgrade-Insecure-Requests": "1",
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
CHROME110 = BrowserProfile(
|
|
156
|
+
name="chrome110",
|
|
157
|
+
curl_impersonate="chrome110",
|
|
158
|
+
h2_settings={
|
|
159
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
160
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
161
|
+
"INITIAL_WINDOW_SIZE": 6291456,
|
|
162
|
+
"MAX_FRAME_SIZE": 16384,
|
|
163
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
164
|
+
},
|
|
165
|
+
http_headers={
|
|
166
|
+
"User-Agent": (
|
|
167
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
168
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
169
|
+
"Chrome/110.0.0.0 Safari/537.36"
|
|
170
|
+
),
|
|
171
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
172
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
173
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
174
|
+
"Sec-Ch-Ua": '"Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"',
|
|
175
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
176
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
177
|
+
"Sec-Fetch-Dest": "document",
|
|
178
|
+
"Sec-Fetch-Mode": "navigate",
|
|
179
|
+
"Sec-Fetch-Site": "none",
|
|
180
|
+
"Sec-Fetch-User": "?1",
|
|
181
|
+
"Upgrade-Insecure-Requests": "1",
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
FIREFOX133 = BrowserProfile(
|
|
186
|
+
name="firefox133",
|
|
187
|
+
curl_impersonate="firefox133",
|
|
188
|
+
h2_settings={
|
|
189
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
190
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
191
|
+
"INITIAL_WINDOW_SIZE": 131072,
|
|
192
|
+
"MAX_FRAME_SIZE": 16384,
|
|
193
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
194
|
+
},
|
|
195
|
+
http_headers={
|
|
196
|
+
"User-Agent": (
|
|
197
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) "
|
|
198
|
+
"Gecko/20100101 Firefox/133.0"
|
|
199
|
+
),
|
|
200
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
201
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
202
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
203
|
+
"Sec-Fetch-Dest": "document",
|
|
204
|
+
"Sec-Fetch-Mode": "navigate",
|
|
205
|
+
"Sec-Fetch-Site": "none",
|
|
206
|
+
"Sec-Fetch-User": "?1",
|
|
207
|
+
"Upgrade-Insecure-Requests": "1",
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
FIREFOX131 = BrowserProfile(
|
|
212
|
+
name="firefox131",
|
|
213
|
+
curl_impersonate="firefox131",
|
|
214
|
+
h2_settings={
|
|
215
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
216
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
217
|
+
"INITIAL_WINDOW_SIZE": 131072,
|
|
218
|
+
"MAX_FRAME_SIZE": 16384,
|
|
219
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
220
|
+
},
|
|
221
|
+
http_headers={
|
|
222
|
+
"User-Agent": (
|
|
223
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) "
|
|
224
|
+
"Gecko/20100101 Firefox/131.0"
|
|
225
|
+
),
|
|
226
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
227
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
228
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
229
|
+
"Sec-Fetch-Dest": "document",
|
|
230
|
+
"Sec-Fetch-Mode": "navigate",
|
|
231
|
+
"Sec-Fetch-Site": "none",
|
|
232
|
+
"Sec-Fetch-User": "?1",
|
|
233
|
+
"Upgrade-Insecure-Requests": "1",
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
FIREFOX120 = BrowserProfile(
|
|
238
|
+
name="firefox120",
|
|
239
|
+
curl_impersonate="firefox120",
|
|
240
|
+
h2_settings={
|
|
241
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
242
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
243
|
+
"INITIAL_WINDOW_SIZE": 131072,
|
|
244
|
+
"MAX_FRAME_SIZE": 16384,
|
|
245
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
246
|
+
},
|
|
247
|
+
http_headers={
|
|
248
|
+
"User-Agent": (
|
|
249
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "
|
|
250
|
+
"Gecko/20100101 Firefox/120.0"
|
|
251
|
+
),
|
|
252
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
253
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
254
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
255
|
+
"Sec-Fetch-Dest": "document",
|
|
256
|
+
"Sec-Fetch-Mode": "navigate",
|
|
257
|
+
"Sec-Fetch-Site": "none",
|
|
258
|
+
"Sec-Fetch-User": "?1",
|
|
259
|
+
"Upgrade-Insecure-Requests": "1",
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
SAFARI18_0 = BrowserProfile(
|
|
264
|
+
name="safari18_0",
|
|
265
|
+
h2_settings={
|
|
266
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
267
|
+
"MAX_CONCURRENT_STREAMS": 100,
|
|
268
|
+
"INITIAL_WINDOW_SIZE": 4194304,
|
|
269
|
+
"MAX_FRAME_SIZE": 16384,
|
|
270
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
271
|
+
},
|
|
272
|
+
http_headers={
|
|
273
|
+
"User-Agent": (
|
|
274
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
275
|
+
"AppleWebKit/605.1.15 (KHTML, like Gecko) "
|
|
276
|
+
"Version/18.0 Safari/605.1.15"
|
|
277
|
+
),
|
|
278
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
279
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
280
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
SAMSUNG_ANDROID = BrowserProfile(
|
|
285
|
+
name="samsung_android",
|
|
286
|
+
curl_impersonate="samsung_android",
|
|
287
|
+
h2_settings={
|
|
288
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
289
|
+
"MAX_CONCURRENT_STREAMS": 100,
|
|
290
|
+
"INITIAL_WINDOW_SIZE": 65536,
|
|
291
|
+
"MAX_FRAME_SIZE": 16384,
|
|
292
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
293
|
+
},
|
|
294
|
+
http_headers={
|
|
295
|
+
"User-Agent": (
|
|
296
|
+
"Mozilla/5.0 (Linux; Android 13; SM-S911B) "
|
|
297
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
298
|
+
"SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36"
|
|
299
|
+
),
|
|
300
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
301
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
302
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
EDGE131 = BrowserProfile(
|
|
307
|
+
name="edge131",
|
|
308
|
+
curl_impersonate="edge131",
|
|
309
|
+
h2_settings={
|
|
310
|
+
"HEADER_TABLE_SIZE": 65536,
|
|
311
|
+
"MAX_CONCURRENT_STREAMS": 1000,
|
|
312
|
+
"INITIAL_WINDOW_SIZE": 6291456,
|
|
313
|
+
"MAX_FRAME_SIZE": 16384,
|
|
314
|
+
"MAX_HEADER_LIST_SIZE": 262144,
|
|
315
|
+
},
|
|
316
|
+
http_headers={
|
|
317
|
+
"User-Agent": (
|
|
318
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
319
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
320
|
+
"Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
|
|
321
|
+
),
|
|
322
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
323
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
324
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
325
|
+
"Sec-Ch-Ua": '"Chromium";v="131", "Microsoft Edge";v="131", "Not_A Brand";v="24"',
|
|
326
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
327
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
328
|
+
"Sec-Fetch-Dest": "document",
|
|
329
|
+
"Sec-Fetch-Mode": "navigate",
|
|
330
|
+
"Sec-Fetch-Site": "none",
|
|
331
|
+
"Sec-Fetch-User": "?1",
|
|
332
|
+
"Upgrade-Insecure-Requests": "1",
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# ── Profile registry ─────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
PROFILES: dict[str, BrowserProfile] = {
|
|
339
|
+
"chrome131": CHROME131,
|
|
340
|
+
"chrome124": CHROME124,
|
|
341
|
+
"chrome120": CHROME120,
|
|
342
|
+
"chrome110": CHROME110,
|
|
343
|
+
"firefox133": FIREFOX133,
|
|
344
|
+
"firefox131": FIREFOX131,
|
|
345
|
+
"firefox120": FIREFOX120,
|
|
346
|
+
"safari18.0": SAFARI18_0,
|
|
347
|
+
"samsung_android": SAMSUNG_ANDROID,
|
|
348
|
+
"edge131": EDGE131,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Aliases for curlcffi compatibility
|
|
352
|
+
CHROME_ALIASES = {
|
|
353
|
+
"chrome": CHROME131,
|
|
354
|
+
"chrome99": CHROME131, # closest available
|
|
355
|
+
"chrome100": CHROME131,
|
|
356
|
+
"chrome101": CHROME131,
|
|
357
|
+
"chrome104": CHROME131,
|
|
358
|
+
"chrome107": CHROME131,
|
|
359
|
+
"chrome110": CHROME110,
|
|
360
|
+
"chrome116": CHROME120,
|
|
361
|
+
"chrome119": CHROME120,
|
|
362
|
+
"chrome120": CHROME120,
|
|
363
|
+
"chrome123": CHROME124,
|
|
364
|
+
"chrome124": CHROME124,
|
|
365
|
+
"chrome131": CHROME131,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
FIREFOX_ALIASES = {
|
|
369
|
+
"firefox": FIREFOX133,
|
|
370
|
+
"firefox99": FIREFOX120,
|
|
371
|
+
"firefox100": FIREFOX120,
|
|
372
|
+
"firefox102": FIREFOX120,
|
|
373
|
+
"firefox109": FIREFOX120,
|
|
374
|
+
"firefox117": FIREFOX120,
|
|
375
|
+
"firefox119": FIREFOX120,
|
|
376
|
+
"firefox120": FIREFOX120,
|
|
377
|
+
"firefox131": FIREFOX131,
|
|
378
|
+
"firefox133": FIREFOX133,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
ALL_ALIASES = {**CHROME_ALIASES, **FIREFOX_ALIASES}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def get_profile(name: str) -> BrowserProfile:
|
|
385
|
+
"""
|
|
386
|
+
Get a browser profile by name.
|
|
387
|
+
|
|
388
|
+
Supports both exact names and curlcffi-compatible aliases.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
name: Profile name (e.g., "chrome131", "firefox120", "chrome")
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
BrowserProfile instance
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
ValueError: If profile name is not recognized
|
|
398
|
+
"""
|
|
399
|
+
# Normalize: strip "chrome" prefix variations
|
|
400
|
+
normalized = name.lower().strip()
|
|
401
|
+
|
|
402
|
+
# Check direct match first
|
|
403
|
+
if normalized in PROFILES:
|
|
404
|
+
return PROFILES[normalized]
|
|
405
|
+
|
|
406
|
+
# Check aliases
|
|
407
|
+
if normalized in ALL_ALIASES:
|
|
408
|
+
return ALL_ALIASES[normalized]
|
|
409
|
+
|
|
410
|
+
# Try fuzzy match
|
|
411
|
+
for profile_name, profile in PROFILES.items():
|
|
412
|
+
if profile_name in normalized or normalized in profile_name:
|
|
413
|
+
return profile
|
|
414
|
+
|
|
415
|
+
available = ", ".join(sorted(set(list(PROFILES.keys()) + list(ALL_ALIASES.keys()))))
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"Unknown browser profile: {name!r}. "
|
|
418
|
+
f"Available profiles: {available}"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def list_profiles() -> list[str]:
|
|
423
|
+
"""List all available profile names."""
|
|
424
|
+
return sorted(set(list(PROFILES.keys()) + list(ALL_ALIASES.keys())))
|