@hyperttp/transport-bun 0.2.2 → 0.2.4
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.
- package/README.md +138 -56
- package/bun.d.ts +92 -48
- package/bun.d.ts.map +1 -1
- package/bun.js +355 -229
- package/bun.js.map +1 -1
- package/package.json +3 -2
- package/types/index.d.ts +18 -0
- package/types/index.d.ts.map +1 -0
- package/types/index.js +2 -0
- package/types/index.js.map +1 -0
- package/utils/helpers.d.ts +35 -0
- package/utils/helpers.d.ts.map +1 -0
- package/utils/helpers.js +129 -0
- package/utils/helpers.js.map +1 -0
package/bun.js
CHANGED
|
@@ -1,144 +1,174 @@
|
|
|
1
|
+
import { fastGetHostname, getAbortError, normalizeHeaders, resolveUrl, TIMEOUT_ERROR, } from "./utils/helpers.js";
|
|
2
|
+
import { CacheManager } from "hcacher";
|
|
1
3
|
/**
|
|
2
|
-
* @ru
|
|
3
|
-
*
|
|
4
|
+
* @ru Статические пресеты браузерных заголовков для маскировки под реальных пользователей.
|
|
5
|
+
* Используются stealth-режимом для обхода fingerprint-защит.
|
|
6
|
+
* @en Static presets of browser headers for masking as real users.
|
|
7
|
+
* Used by stealth mode to bypass fingerprint protections.
|
|
4
8
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
const STEALTH_HEADER_PRESETS = {
|
|
10
|
+
chrome: {
|
|
11
|
+
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
|
|
12
|
+
"sec-ch-ua-mobile": "?0",
|
|
13
|
+
"sec-ch-ua-platform": '"Linux"',
|
|
14
|
+
"sec-fetch-dest": "document",
|
|
15
|
+
"sec-fetch-mode": "navigate",
|
|
16
|
+
"sec-fetch-site": "none",
|
|
17
|
+
"sec-fetch-user": "?1",
|
|
18
|
+
"upgrade-insecure-requests": "1",
|
|
19
|
+
"accept-language": "en-US,en;q=0.9",
|
|
20
|
+
},
|
|
21
|
+
firefox: {
|
|
22
|
+
"sec-fetch-dest": "document",
|
|
23
|
+
"sec-fetch-mode": "navigate",
|
|
24
|
+
"sec-fetch-site": "none",
|
|
25
|
+
"accept-language": "en-US,en;q=0.5",
|
|
26
|
+
"upgrade-insecure-requests": "1",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
9
29
|
/**
|
|
10
|
-
* @ru
|
|
11
|
-
* @en
|
|
12
|
-
* @param url - The URL to extract hostname from.
|
|
13
|
-
* @returns The extracted hostname.
|
|
30
|
+
* @ru Пресеты User-Agent, соответствующие TLS-отпечаткам (JA3/JA4).
|
|
31
|
+
* @en User-Agent presets matching the TLS fingerprints (JA3/JA4).
|
|
14
32
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (cached !== undefined) {
|
|
22
|
-
lastUrl = url;
|
|
23
|
-
lastHost = cached;
|
|
24
|
-
return cached;
|
|
25
|
-
}
|
|
26
|
-
let start = 0;
|
|
27
|
-
const first = url.charCodeAt(0);
|
|
28
|
-
if (first === 47) {
|
|
29
|
-
start = url.charCodeAt(1) === 47 ? 2 : 0;
|
|
30
|
-
if (start === 0)
|
|
31
|
-
return "localhost";
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
const schemeIdx = url.indexOf("://");
|
|
35
|
-
if (schemeIdx !== -1)
|
|
36
|
-
start = schemeIdx + 3;
|
|
37
|
-
}
|
|
38
|
-
let end = url.length;
|
|
39
|
-
for (let i = start; i < end; i++) {
|
|
40
|
-
const code = url.charCodeAt(i);
|
|
41
|
-
if (code === 47 || code === 63 || code === 35) {
|
|
42
|
-
end = i;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const atIdx = url.indexOf("@", start);
|
|
47
|
-
if (atIdx !== -1 && atIdx < end)
|
|
48
|
-
start = atIdx + 1;
|
|
49
|
-
const colonIdx = url.indexOf(":", start);
|
|
50
|
-
if (colonIdx !== -1 && colonIdx < end)
|
|
51
|
-
end = colonIdx;
|
|
52
|
-
const host = url.slice(start, end) || "localhost";
|
|
53
|
-
if (hostnameCacheSize > 1024) {
|
|
54
|
-
hostnameCache = Object.create(null);
|
|
55
|
-
hostnameCacheSize = 0;
|
|
56
|
-
}
|
|
57
|
-
hostnameCache[url] = host;
|
|
58
|
-
hostnameCacheSize++;
|
|
59
|
-
lastUrl = url;
|
|
60
|
-
lastHost = host;
|
|
61
|
-
return host;
|
|
62
|
-
}
|
|
33
|
+
const STEALTH_UA_PRESETS = {
|
|
34
|
+
chrome: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
|
35
|
+
firefox: "Mozilla/5.0 (X11; Linux; rv:126.0) Gecko/20100101 Firefox/126.0",
|
|
36
|
+
safari: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
|
37
|
+
edge: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
|
|
38
|
+
};
|
|
63
39
|
/**
|
|
64
|
-
* @ru
|
|
65
|
-
* @en
|
|
66
|
-
* @param
|
|
67
|
-
* @
|
|
68
|
-
* @returns The resolved absolute URL.
|
|
40
|
+
* @ru Возвращает строку шифров TLS для указанного профиля браузера.
|
|
41
|
+
* @en Returns the TLS cipher suite string for the specified browser profile.
|
|
42
|
+
* @param fingerprint - Browser fingerprint profile.
|
|
43
|
+
* @returns Colon-separated cipher suite string, or empty string if not found.
|
|
69
44
|
*/
|
|
70
|
-
function
|
|
71
|
-
if (
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
45
|
+
function getCiphersForProfile(fingerprint) {
|
|
46
|
+
if (!fingerprint)
|
|
47
|
+
return "";
|
|
48
|
+
switch (fingerprint) {
|
|
49
|
+
case "chrome":
|
|
50
|
+
case "edge":
|
|
51
|
+
return [
|
|
52
|
+
"TLS_AES_128_GCM_SHA256",
|
|
53
|
+
"TLS_AES_256_GCM_SHA384",
|
|
54
|
+
"TLS_CHACHA20_POLY1305_SHA256",
|
|
55
|
+
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
|
56
|
+
"ECDHE-RSA-AES128-GCM-SHA256",
|
|
57
|
+
].join(":");
|
|
58
|
+
case "firefox":
|
|
59
|
+
return [
|
|
60
|
+
"TLS_AES_128_GCM_SHA256",
|
|
61
|
+
"TLS_CHACHA20_POLY1305_SHA256",
|
|
62
|
+
"TLS_AES_256_GCM_SHA384",
|
|
63
|
+
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
|
64
|
+
"ECDHE-RSA-AES128-GCM-SHA256",
|
|
65
|
+
].join(":");
|
|
66
|
+
case "safari":
|
|
67
|
+
return [
|
|
68
|
+
"TLS_AES_256_GCM_SHA384",
|
|
69
|
+
"TLS_CHACHA20_POLY1305_SHA256",
|
|
70
|
+
"TLS_AES_128_GCM_SHA256",
|
|
71
|
+
"ECDHE-ECDSA-AES256-GCM-SHA384",
|
|
72
|
+
"ECDHE-RSA-AES256-GCM-SHA384",
|
|
73
|
+
].join(":");
|
|
74
|
+
default:
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
91
77
|
}
|
|
92
78
|
/**
|
|
93
|
-
* @ru
|
|
94
|
-
* @en
|
|
95
|
-
* @param
|
|
96
|
-
* @
|
|
79
|
+
* @ru Безопасно применяет стелс-пресеты, отдавая абсолютный приоритет ручным заголовкам.
|
|
80
|
+
* @en Safely applies stealth presets, giving absolute priority to manual headers.
|
|
81
|
+
* @param headers - The headers object to modify.
|
|
82
|
+
* @param stealth - Stealth configuration options.
|
|
83
|
+
* @returns The modified headers object.
|
|
97
84
|
*/
|
|
98
|
-
function
|
|
99
|
-
if (
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return JSON.stringify(body);
|
|
119
|
-
return String(body);
|
|
85
|
+
function applyStealthHeaders(headers, stealth) {
|
|
86
|
+
if (!stealth || !stealth.fingerprint)
|
|
87
|
+
return headers;
|
|
88
|
+
const presetName = stealth.fingerprint;
|
|
89
|
+
const presetHeaders = STEALTH_HEADER_PRESETS[presetName];
|
|
90
|
+
if (presetHeaders) {
|
|
91
|
+
for (const key in presetHeaders) {
|
|
92
|
+
if (headers[key] === undefined) {
|
|
93
|
+
headers[key] = presetHeaders[key];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const currentUA = headers["user-agent"];
|
|
98
|
+
if (currentUA === undefined || currentUA === "hyperttp/2.0" || currentUA === "Hyperttp/2.0") {
|
|
99
|
+
const browserUA = STEALTH_UA_PRESETS[presetName];
|
|
100
|
+
if (browserUA) {
|
|
101
|
+
headers["user-agent"] = browserUA;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return headers;
|
|
120
105
|
}
|
|
121
106
|
/**
|
|
122
|
-
* @ru Высокопроизводительный HTTP-транспорт для Bun с нативными
|
|
123
|
-
*
|
|
124
|
-
* @en High-performance HTTP transport for Bun with native optimizations.
|
|
125
|
-
*
|
|
107
|
+
* @ru Высокопроизводительный HTTP-транспорт для Bun с нативными оптимизациями и поддержкой Stealth-мимикрии.
|
|
108
|
+
* Включает управление параллелизмом, cookie jar и кэширование доменов.
|
|
109
|
+
* @en High-performance HTTP transport for Bun with native optimizations and Stealth mimicry support.
|
|
110
|
+
* Includes concurrency management, cookie jar, and domain caching.
|
|
126
111
|
*/
|
|
127
112
|
export class BunTransport {
|
|
113
|
+
/**
|
|
114
|
+
* @ru Конфигурация транспорта.
|
|
115
|
+
* @en Transport configuration.
|
|
116
|
+
*/
|
|
128
117
|
config;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
/**
|
|
119
|
+
* @ru Кэш cookies по доменам с TTL и LRU-эвикцией (domain -> name -> value).
|
|
120
|
+
* @en Cookie cache by domain with TTL and LRU eviction (domain -> name -> value).
|
|
121
|
+
*/
|
|
122
|
+
cookieStore;
|
|
123
|
+
/**
|
|
124
|
+
* @ru Кэш сгенерированных строк cookies с TTL.
|
|
125
|
+
* @en Cache of generated cookie strings with TTL.
|
|
126
|
+
*/
|
|
127
|
+
cookieStringCache;
|
|
128
|
+
/**
|
|
129
|
+
* @ru Опциональный кэш HTTP-ответов для GET/HEAD запросов.
|
|
130
|
+
* @en Optional HTTP response cache for GET/HEAD requests.
|
|
131
|
+
*/
|
|
132
|
+
responseCache;
|
|
133
|
+
/**
|
|
134
|
+
* @ru Счётчик активных (выполняющихся) запросов.
|
|
135
|
+
* @en Counter of active (in-flight) requests.
|
|
136
|
+
*/
|
|
133
137
|
activeRequests = 0;
|
|
138
|
+
/**
|
|
139
|
+
* @ru Очередь запросов, ожидающих свободного слота при достижении лимита параллелизма.
|
|
140
|
+
* @en Queue of requests waiting for a free slot when concurrency limit is reached.
|
|
141
|
+
*/
|
|
134
142
|
concurrencyQueue = Object.create(null);
|
|
143
|
+
/**
|
|
144
|
+
* @ru Индекс начала очереди (для FIFO обработки).
|
|
145
|
+
* @en Queue head index (for FIFO processing).
|
|
146
|
+
*/
|
|
135
147
|
queueHead = 0;
|
|
148
|
+
/**
|
|
149
|
+
* @ru Индекс конца очереди (для добавления новых элементов).
|
|
150
|
+
* @en Queue tail index (for adding new elements).
|
|
151
|
+
*/
|
|
136
152
|
queueTail = 0;
|
|
137
|
-
|
|
153
|
+
/**
|
|
154
|
+
* @ru Максимальное количество одновременных запросов (0 = без лимита).
|
|
155
|
+
* @en Maximum number of concurrent requests (0 = unlimited).
|
|
156
|
+
*/
|
|
138
157
|
_maxConcurrent = 0;
|
|
158
|
+
/**
|
|
159
|
+
* @ru Таймаут запроса в миллисекундах (0 = без таймаута).
|
|
160
|
+
* @en Request timeout in milliseconds (0 = no timeout).
|
|
161
|
+
*/
|
|
139
162
|
_timeout = 0;
|
|
140
|
-
|
|
163
|
+
/**
|
|
164
|
+
* @ru Флаг использования keep-alive соединений.
|
|
165
|
+
* @en Flag for using keep-alive connections.
|
|
166
|
+
*/
|
|
141
167
|
_keepalive = false;
|
|
168
|
+
/**
|
|
169
|
+
* @ru Конфигурация TLS (отклонение невалидных сертификатов и шифры).
|
|
170
|
+
* @en TLS configuration (reject unauthorized certificates and ciphers).
|
|
171
|
+
*/
|
|
142
172
|
_tlsConfig = null;
|
|
143
173
|
/**
|
|
144
174
|
* @ru Создаёт экземпляр BunTransport.
|
|
@@ -147,31 +177,57 @@ export class BunTransport {
|
|
|
147
177
|
*/
|
|
148
178
|
constructor(config) {
|
|
149
179
|
this.config = config;
|
|
180
|
+
const cookieCfg = config?.network?.cookieCache;
|
|
181
|
+
this.cookieStore = new CacheManager({
|
|
182
|
+
enabled: cookieCfg?.enabled ?? true,
|
|
183
|
+
maxSize: cookieCfg?.maxSize ?? 256,
|
|
184
|
+
ttl: cookieCfg?.ttl ?? 300_000,
|
|
185
|
+
touchOnGet: true,
|
|
186
|
+
});
|
|
187
|
+
this.cookieStringCache = new CacheManager({
|
|
188
|
+
enabled: cookieCfg?.enabled ?? true,
|
|
189
|
+
maxSize: cookieCfg?.maxSize ?? 1024,
|
|
190
|
+
ttl: cookieCfg?.ttl ?? 60_000,
|
|
191
|
+
touchOnGet: true,
|
|
192
|
+
});
|
|
193
|
+
const cacheCfg = config?.network?.cache;
|
|
194
|
+
if (cacheCfg?.enabled !== false && (cacheCfg?.maxSize || cacheCfg?.ttl)) {
|
|
195
|
+
this.responseCache = new CacheManager({
|
|
196
|
+
enabled: cacheCfg?.enabled ?? true,
|
|
197
|
+
maxSize: cacheCfg?.maxSize ?? 256,
|
|
198
|
+
ttl: cacheCfg?.ttl ?? 30_000,
|
|
199
|
+
touchOnGet: true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
150
202
|
this.invalidateConfig();
|
|
151
203
|
}
|
|
152
204
|
/**
|
|
153
|
-
* @ru
|
|
154
|
-
*
|
|
205
|
+
* @ru Обновляет внутреннее состояние на основе конфигурации.
|
|
206
|
+
* Вызывается при создании и может быть вызвано при изменении конфига.
|
|
207
|
+
* @en Updates internal state based on configuration.
|
|
208
|
+
* Called on creation and can be called when config changes.
|
|
155
209
|
*/
|
|
156
210
|
invalidateConfig() {
|
|
157
211
|
const net = this.config?.network;
|
|
158
212
|
this._maxConcurrent = net?.maxConcurrent ?? 0;
|
|
159
213
|
this._timeout = net?.timeout ?? 0;
|
|
160
|
-
this._userAgent = net?.userAgent;
|
|
161
214
|
this._keepalive = !!net?.keepAliveTimeout;
|
|
162
215
|
this._tlsConfig = (net?.rejectUnauthorized ?? true) ? null : { rejectUnauthorized: false };
|
|
163
216
|
}
|
|
164
217
|
/**
|
|
165
|
-
* @ru Выполняет HTTP-запрос через нативный fetch Bun
|
|
166
|
-
* @en Executes an HTTP request via Bun's native fetch.
|
|
218
|
+
* @ru Выполняет HTTP-запрос через нативный fetch Bun с управлением параллелизмом и таймаутами.
|
|
219
|
+
* @en Executes an HTTP request via Bun's native fetch with concurrency management and timeouts.
|
|
167
220
|
* @param req - The normalized transport request.
|
|
168
221
|
* @returns Promise resolving to the transport response.
|
|
222
|
+
* @throws Error if the request is aborted or times out.
|
|
169
223
|
*/
|
|
170
224
|
async execute(req) {
|
|
171
225
|
const maxConcurrent = this._maxConcurrent;
|
|
172
226
|
const timeoutMs = this._timeout;
|
|
173
227
|
let signal = req.signal;
|
|
174
228
|
let timer = null;
|
|
229
|
+
let abortHandler = null;
|
|
230
|
+
let originalSignal = null;
|
|
175
231
|
if (timeoutMs > 0) {
|
|
176
232
|
if (!signal) {
|
|
177
233
|
signal = AbortSignal.timeout(timeoutMs);
|
|
@@ -181,102 +237,209 @@ export class BunTransport {
|
|
|
181
237
|
}
|
|
182
238
|
else {
|
|
183
239
|
const controller = new AbortController();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
240
|
+
originalSignal = signal;
|
|
241
|
+
abortHandler = () => controller.abort(originalSignal.reason);
|
|
242
|
+
originalSignal.addEventListener("abort", abortHandler);
|
|
188
243
|
timer = setTimeout(() => controller.abort(TIMEOUT_ERROR), timeoutMs);
|
|
189
244
|
signal = controller.signal;
|
|
190
245
|
}
|
|
191
246
|
}
|
|
192
|
-
if (signal?.aborted)
|
|
247
|
+
if (signal?.aborted) {
|
|
248
|
+
if (timer !== null)
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
if (originalSignal && abortHandler)
|
|
251
|
+
originalSignal.removeEventListener("abort", abortHandler);
|
|
193
252
|
throw getAbortError(signal);
|
|
253
|
+
}
|
|
194
254
|
if (maxConcurrent > 0 && this.activeRequests >= maxConcurrent) {
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
try {
|
|
256
|
+
await this.waitForSlot(signal);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
if (timer !== null)
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
if (originalSignal && abortHandler)
|
|
262
|
+
originalSignal.removeEventListener("abort", abortHandler);
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
if (signal?.aborted) {
|
|
266
|
+
if (timer !== null)
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
if (originalSignal && abortHandler)
|
|
269
|
+
originalSignal.removeEventListener("abort", abortHandler);
|
|
197
270
|
throw getAbortError(signal);
|
|
271
|
+
}
|
|
198
272
|
}
|
|
199
273
|
this.activeRequests++;
|
|
200
274
|
try {
|
|
201
|
-
const headers = this.prepareHeaders(req);
|
|
202
275
|
const fullUrl = resolveUrl(this.config?.baseUrl ?? "", req.url);
|
|
276
|
+
if (this.responseCache && (req.method === "GET" || req.method === "HEAD") && !req.body) {
|
|
277
|
+
const cacheKey = `${req.method}:${fullUrl}`;
|
|
278
|
+
const cached = this.responseCache.get(cacheKey);
|
|
279
|
+
if (cached !== undefined) {
|
|
280
|
+
return cached;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
let headers = normalizeHeaders(req.headers);
|
|
284
|
+
const requestDomain = fastGetHostname(fullUrl);
|
|
285
|
+
const activeCookies = this.getCookiesForDomain(requestDomain);
|
|
286
|
+
if (activeCookies.length > 0) {
|
|
287
|
+
headers["cookie"] = headers["cookie"]
|
|
288
|
+
? headers["cookie"] + "; " + activeCookies
|
|
289
|
+
: activeCookies;
|
|
290
|
+
}
|
|
291
|
+
const stealth = req.stealth || this.config.stealth ? { ...this.config.stealth, ...req.stealth } : undefined;
|
|
292
|
+
if (stealth) {
|
|
293
|
+
headers = applyStealthHeaders(headers, stealth);
|
|
294
|
+
}
|
|
203
295
|
const init = {
|
|
204
296
|
method: req.method,
|
|
205
|
-
redirect: "manual",
|
|
297
|
+
redirect: req.redirect ?? "manual",
|
|
298
|
+
headers: headers,
|
|
206
299
|
};
|
|
207
|
-
if (headers !== undefined)
|
|
208
|
-
init.headers = headers;
|
|
209
300
|
if (req.body !== undefined)
|
|
210
|
-
init.body =
|
|
301
|
+
init.body = req.body;
|
|
211
302
|
if (signal !== undefined)
|
|
212
303
|
init.signal = signal;
|
|
213
304
|
if (this._keepalive)
|
|
214
305
|
init.keepalive = true;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
306
|
+
let requestTlsConfig = this._tlsConfig;
|
|
307
|
+
if (stealth?.ciphers || stealth?.fingerprint) {
|
|
308
|
+
const ciphers = stealth.ciphers ?? getCiphersForProfile(stealth.fingerprint);
|
|
309
|
+
if (ciphers) {
|
|
310
|
+
requestTlsConfig = requestTlsConfig
|
|
311
|
+
? { ...requestTlsConfig, ciphers }
|
|
312
|
+
: { rejectUnauthorized: true, ciphers };
|
|
313
|
+
}
|
|
221
314
|
}
|
|
222
|
-
|
|
315
|
+
if (requestTlsConfig !== null)
|
|
316
|
+
init.tls = requestTlsConfig;
|
|
317
|
+
const nativeRes = await fetch(fullUrl, init);
|
|
318
|
+
this.storeCookies(fullUrl, nativeRes.headers);
|
|
319
|
+
const bodyStream = nativeRes.body;
|
|
320
|
+
const responseHeaders = (() => {
|
|
321
|
+
const resH = Object.create(null);
|
|
322
|
+
nativeRes.headers.forEach((value, key) => {
|
|
323
|
+
resH[key.toLowerCase()] = value;
|
|
324
|
+
});
|
|
325
|
+
return resH;
|
|
326
|
+
})();
|
|
327
|
+
const response = {
|
|
223
328
|
status: nativeRes.status,
|
|
224
329
|
url: nativeRes.url,
|
|
225
|
-
body:
|
|
226
|
-
headers:
|
|
330
|
+
body: bodyStream,
|
|
331
|
+
headers: responseHeaders,
|
|
227
332
|
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
333
|
+
if (this.responseCache && (req.method === "GET" || req.method === "HEAD") && !req.body) {
|
|
334
|
+
const cacheKey = `${req.method}:${fullUrl}`;
|
|
335
|
+
this.responseCache.set(cacheKey, response);
|
|
336
|
+
}
|
|
337
|
+
return response;
|
|
233
338
|
}
|
|
234
339
|
finally {
|
|
340
|
+
if (timer !== null) {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
}
|
|
343
|
+
if (originalSignal && abortHandler) {
|
|
344
|
+
originalSignal.removeEventListener("abort", abortHandler);
|
|
345
|
+
}
|
|
235
346
|
this.activeRequests--;
|
|
236
347
|
if (maxConcurrent > 0)
|
|
237
348
|
this.releaseSlot();
|
|
238
349
|
}
|
|
239
350
|
}
|
|
240
351
|
/**
|
|
241
|
-
* @ru
|
|
242
|
-
* @en
|
|
243
|
-
* @param
|
|
244
|
-
* @returns
|
|
352
|
+
* @ru Получает строку cookies для указанного домена с учётом родительских доменов.
|
|
353
|
+
* @en Gets the cookie string for the specified domain, considering parent domains.
|
|
354
|
+
* @param requestDomain - The domain to get cookies for.
|
|
355
|
+
* @returns Semicolon-separated cookie string.
|
|
245
356
|
*/
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
357
|
+
getCookiesForDomain(requestDomain) {
|
|
358
|
+
const cached = this.cookieStringCache.get(requestDomain);
|
|
359
|
+
if (cached !== undefined)
|
|
360
|
+
return cached;
|
|
361
|
+
let result = "";
|
|
362
|
+
const domains = this.cookieStore.size > 0 ? this.getAllCookieDomains() : [];
|
|
363
|
+
for (let i = 0; i < domains.length; i++) {
|
|
364
|
+
const storedDomain = domains[i];
|
|
365
|
+
if (requestDomain !== storedDomain && !requestDomain.endsWith("." + storedDomain))
|
|
366
|
+
continue;
|
|
367
|
+
const cookiesMap = this.cookieStore.get(storedDomain);
|
|
368
|
+
if (!cookiesMap)
|
|
369
|
+
continue;
|
|
370
|
+
for (const key in cookiesMap) {
|
|
371
|
+
if (result.length > 0)
|
|
372
|
+
result += "; ";
|
|
373
|
+
result += key + "=" + cookiesMap[key];
|
|
260
374
|
}
|
|
261
|
-
return headers;
|
|
262
375
|
}
|
|
263
|
-
|
|
264
|
-
|
|
376
|
+
this.cookieStringCache.set(requestDomain, result);
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* @ru Возвращает все домены из cookie store.
|
|
381
|
+
* @en Returns all domains from the cookie store.
|
|
382
|
+
*/
|
|
383
|
+
getAllCookieDomains() {
|
|
384
|
+
const domains = [];
|
|
385
|
+
const store = this.cookieStore.storage;
|
|
386
|
+
for (const key of store.keys()) {
|
|
387
|
+
domains.push(key);
|
|
265
388
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
389
|
+
return domains;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* @ru Сохраняет cookies из заголовков Set-Cookie ответа в cookie jar.
|
|
393
|
+
* @en Stores cookies from response Set-Cookie headers into the cookie jar.
|
|
394
|
+
* @param requestUrl - The request URL for determining the default domain.
|
|
395
|
+
* @param headers - Response headers containing Set-Cookie values.
|
|
396
|
+
*/
|
|
397
|
+
storeCookies(requestUrl, headers) {
|
|
398
|
+
if (typeof headers.getSetCookie !== "function")
|
|
399
|
+
return;
|
|
400
|
+
const setCookies = headers.getSetCookie();
|
|
401
|
+
if (setCookies.length === 0)
|
|
402
|
+
return;
|
|
403
|
+
const defaultDomain = fastGetHostname(requestUrl);
|
|
404
|
+
for (let i = 0; i < setCookies.length; i++) {
|
|
405
|
+
const rawCookie = setCookies[i];
|
|
406
|
+
if (!rawCookie)
|
|
407
|
+
continue;
|
|
408
|
+
const firstSemicolon = rawCookie.indexOf(";");
|
|
409
|
+
const mainPair = firstSemicolon === -1 ? rawCookie : rawCookie.slice(0, firstSemicolon);
|
|
410
|
+
const eqIdx = mainPair.indexOf("=");
|
|
411
|
+
if (eqIdx === -1)
|
|
412
|
+
continue;
|
|
413
|
+
const name = mainPair.slice(0, eqIdx).trim();
|
|
414
|
+
const value = mainPair.slice(eqIdx + 1).trim();
|
|
415
|
+
if (!name)
|
|
416
|
+
continue;
|
|
417
|
+
let domain = defaultDomain;
|
|
418
|
+
if (firstSemicolon !== -1) {
|
|
419
|
+
const parts = rawCookie.split(";");
|
|
420
|
+
for (let j = 1; j < parts.length; j++) {
|
|
421
|
+
const part = parts[j].trim();
|
|
422
|
+
if (part.length > 7 && part.toLowerCase().startsWith("domain=")) {
|
|
423
|
+
const rawDomain = part.slice(7).trim();
|
|
424
|
+
if (rawDomain) {
|
|
425
|
+
domain = rawDomain.charCodeAt(0) === 46 ? rawDomain.slice(1) : rawDomain;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
271
430
|
}
|
|
431
|
+
const existing = this.cookieStore.get(domain) ?? Object.create(null);
|
|
432
|
+
existing[name] = value;
|
|
433
|
+
this.cookieStore.set(domain, existing);
|
|
434
|
+
this.cookieStringCache.delete(domain);
|
|
272
435
|
}
|
|
273
|
-
return original;
|
|
274
436
|
}
|
|
275
437
|
/**
|
|
276
|
-
* @ru Ожидает
|
|
277
|
-
* @en Waits for
|
|
278
|
-
* @param signal - Optional abort signal.
|
|
438
|
+
* @ru Ожидает освобождения слота в очереди параллелизма.
|
|
439
|
+
* @en Waits for a slot to become available in the concurrency queue.
|
|
440
|
+
* @param signal - Optional abort signal to cancel waiting.
|
|
279
441
|
* @returns Promise that resolves when a slot is available.
|
|
442
|
+
* @throws Error if the signal is aborted while waiting.
|
|
280
443
|
*/
|
|
281
444
|
async waitForSlot(signal) {
|
|
282
445
|
return new Promise((resolve, reject) => {
|
|
@@ -295,8 +458,8 @@ export class BunTransport {
|
|
|
295
458
|
});
|
|
296
459
|
}
|
|
297
460
|
/**
|
|
298
|
-
* @ru Освобождает слот и
|
|
299
|
-
* @en Releases a slot and
|
|
461
|
+
* @ru Освобождает слот и разблокирует следующий запрос в очереди.
|
|
462
|
+
* @en Releases a slot and unblocks the next request in the queue.
|
|
300
463
|
*/
|
|
301
464
|
releaseSlot() {
|
|
302
465
|
while (this.queueHead < this.queueTail) {
|
|
@@ -315,60 +478,23 @@ export class BunTransport {
|
|
|
315
478
|
}
|
|
316
479
|
}
|
|
317
480
|
/**
|
|
318
|
-
* @ru
|
|
319
|
-
* @en
|
|
320
|
-
* @
|
|
321
|
-
* @returns The cookie string or empty string.
|
|
322
|
-
*/
|
|
323
|
-
getCookiesForDomain(requestDomain) {
|
|
324
|
-
const cached = this.cookieCache[requestDomain];
|
|
325
|
-
if (cached !== undefined)
|
|
326
|
-
return cached;
|
|
327
|
-
let result = "";
|
|
328
|
-
const domainsLen = this.cookieDomains.length;
|
|
329
|
-
for (let i = 0; i < domainsLen; i++) {
|
|
330
|
-
const storedDomain = this.cookieDomains[i];
|
|
331
|
-
if (!storedDomain)
|
|
332
|
-
continue;
|
|
333
|
-
if (requestDomain !== storedDomain && !requestDomain.endsWith("." + storedDomain))
|
|
334
|
-
continue;
|
|
335
|
-
const cookiesMap = this.cookieJar[storedDomain];
|
|
336
|
-
if (!cookiesMap)
|
|
337
|
-
continue;
|
|
338
|
-
for (const key in cookiesMap) {
|
|
339
|
-
if (result.length > 0)
|
|
340
|
-
result += "; ";
|
|
341
|
-
result += key + "=" + cookiesMap[key];
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (this.cookieCacheSize > 1024) {
|
|
345
|
-
this.cookieCache = Object.create(null);
|
|
346
|
-
this.cookieCacheSize = 0;
|
|
347
|
-
}
|
|
348
|
-
this.cookieCache[requestDomain] = result;
|
|
349
|
-
this.cookieCacheSize++;
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* @ru Мягко закрывает транспорт, очищая очереди и cookies.
|
|
354
|
-
* @en Gracefully closes the transport, clearing queues and cookies.
|
|
355
|
-
* @returns Promise that resolves when the transport is closed.
|
|
481
|
+
* @ru Мягко закрывает транспорт, очищая очереди и cookie jar.
|
|
482
|
+
* @en Gracefully closes the transport, clearing queues and cookie jar.
|
|
483
|
+
* @returns Promise that resolves when cleanup is complete.
|
|
356
484
|
*/
|
|
357
485
|
async close() {
|
|
358
486
|
this.concurrencyQueue = Object.create(null);
|
|
359
487
|
this.queueHead = 0;
|
|
360
488
|
this.queueTail = 0;
|
|
361
|
-
this.
|
|
362
|
-
this.
|
|
363
|
-
this.
|
|
364
|
-
this.cookieCacheSize = 0;
|
|
489
|
+
this.cookieStore.clear();
|
|
490
|
+
this.cookieStringCache.clear();
|
|
491
|
+
this.responseCache?.clear();
|
|
365
492
|
this.activeRequests = 0;
|
|
366
|
-
this.hasCookies = false;
|
|
367
493
|
}
|
|
368
494
|
/**
|
|
369
|
-
* @ru Принудительно уничтожает транспорт
|
|
370
|
-
* @en
|
|
371
|
-
* @returns Promise that resolves when
|
|
495
|
+
* @ru Принудительно уничтожает транспорт (алиас для close()).
|
|
496
|
+
* @en Forcefully destroys the transport (alias for close()).
|
|
497
|
+
* @returns Promise that resolves when cleanup is complete.
|
|
372
498
|
*/
|
|
373
499
|
async destroy() {
|
|
374
500
|
await this.close();
|