httpcloak 1.5.2 → 1.5.3
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 +212 -13
- package/lib/index.d.ts +4 -0
- package/lib/index.js +804 -8
- package/npm/darwin-arm64/package.json +1 -1
- package/npm/darwin-x64/package.json +1 -1
- package/npm/linux-arm64/package.json +1 -1
- package/npm/linux-x64/package.json +1 -1
- package/npm/win32-arm64/package.json +1 -1
- package/npm/win32-x64/package.json +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -48,6 +48,19 @@ async function main() {
|
|
|
48
48
|
main();
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### ES Modules
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { Session } from "httpcloak";
|
|
55
|
+
|
|
56
|
+
const session = new Session({ preset: "chrome-143" });
|
|
57
|
+
|
|
58
|
+
const response = await session.get("https://example.com");
|
|
59
|
+
console.log(response.text);
|
|
60
|
+
|
|
61
|
+
session.close();
|
|
62
|
+
```
|
|
63
|
+
|
|
51
64
|
### Synchronous Usage
|
|
52
65
|
|
|
53
66
|
```javascript
|
|
@@ -99,6 +112,78 @@ session.postCb(
|
|
|
99
112
|
);
|
|
100
113
|
```
|
|
101
114
|
|
|
115
|
+
### Streaming Downloads
|
|
116
|
+
|
|
117
|
+
For large downloads, use streaming to avoid loading entire response into memory:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
const { Session } = require("httpcloak");
|
|
121
|
+
const fs = require("fs");
|
|
122
|
+
|
|
123
|
+
async function downloadFile() {
|
|
124
|
+
const session = new Session({ preset: "chrome-143" });
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Start streaming request
|
|
128
|
+
const stream = session.getStream("https://example.com/large-file.zip");
|
|
129
|
+
|
|
130
|
+
console.log(`Status: ${stream.statusCode}`);
|
|
131
|
+
console.log(`Content-Length: ${stream.contentLength}`);
|
|
132
|
+
console.log(`Protocol: ${stream.protocol}`);
|
|
133
|
+
|
|
134
|
+
// Read in chunks
|
|
135
|
+
const file = fs.createWriteStream("downloaded-file.zip");
|
|
136
|
+
let totalBytes = 0;
|
|
137
|
+
let chunk;
|
|
138
|
+
|
|
139
|
+
while ((chunk = stream.readChunk(65536)) !== null) {
|
|
140
|
+
file.write(chunk);
|
|
141
|
+
totalBytes += chunk.length;
|
|
142
|
+
console.log(`Downloaded ${totalBytes} bytes...`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
file.end();
|
|
146
|
+
stream.close();
|
|
147
|
+
console.log(`Download complete: ${totalBytes} bytes`);
|
|
148
|
+
} finally {
|
|
149
|
+
session.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
downloadFile();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Streaming with All Methods
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const { Session } = require("httpcloak");
|
|
160
|
+
|
|
161
|
+
const session = new Session({ preset: "chrome-143" });
|
|
162
|
+
|
|
163
|
+
// Stream GET
|
|
164
|
+
const getStream = session.getStream("https://example.com/data");
|
|
165
|
+
|
|
166
|
+
// Stream POST
|
|
167
|
+
const postStream = session.postStream("https://example.com/upload", "data");
|
|
168
|
+
|
|
169
|
+
// Stream with custom options
|
|
170
|
+
const customStream = session.requestStream({
|
|
171
|
+
method: "PUT",
|
|
172
|
+
url: "https://example.com/resource",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ key: "value" }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Read response
|
|
178
|
+
let chunk;
|
|
179
|
+
while ((chunk = customStream.readChunk(65536)) !== null) {
|
|
180
|
+
console.log(`Received ${chunk.length} bytes`);
|
|
181
|
+
}
|
|
182
|
+
customStream.close();
|
|
183
|
+
|
|
184
|
+
session.close();
|
|
185
|
+
```
|
|
186
|
+
|
|
102
187
|
## Proxy Support
|
|
103
188
|
|
|
104
189
|
HTTPCloak supports HTTP, SOCKS5, and HTTP/3 (MASQUE) proxies with full fingerprint preservation.
|
|
@@ -165,6 +250,20 @@ const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
|
165
250
|
console.log(response.protocol); // h3
|
|
166
251
|
```
|
|
167
252
|
|
|
253
|
+
### Split Proxy Configuration
|
|
254
|
+
|
|
255
|
+
Use different proxies for TCP (HTTP/1.1, HTTP/2) and UDP (HTTP/3) traffic:
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
const { Session } = require("httpcloak");
|
|
259
|
+
|
|
260
|
+
const session = new Session({
|
|
261
|
+
preset: "chrome-143",
|
|
262
|
+
tcpProxy: "http://tcp-proxy:port", // For HTTP/1.1, HTTP/2
|
|
263
|
+
udpProxy: "https://masque-proxy:port", // For HTTP/3
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
168
267
|
## Advanced Features
|
|
169
268
|
|
|
170
269
|
### Encrypted Client Hello (ECH)
|
|
@@ -225,19 +324,47 @@ const { Session } = require("httpcloak");
|
|
|
225
324
|
|
|
226
325
|
const session = new Session();
|
|
227
326
|
|
|
327
|
+
// Set a cookie
|
|
328
|
+
session.setCookie("session_id", "abc123");
|
|
329
|
+
|
|
228
330
|
// Get all cookies
|
|
229
331
|
const cookies = session.getCookies();
|
|
230
332
|
console.log(cookies);
|
|
231
333
|
|
|
232
|
-
// Set a cookie
|
|
233
|
-
session.setCookie("session_id", "abc123");
|
|
234
|
-
|
|
235
334
|
// Access cookies as property
|
|
236
335
|
console.log(session.cookies);
|
|
237
336
|
|
|
337
|
+
// Clear a cookie
|
|
338
|
+
session.clearCookie("session_id");
|
|
339
|
+
|
|
340
|
+
// Clear all cookies
|
|
341
|
+
session.clearCookies();
|
|
342
|
+
|
|
238
343
|
session.close();
|
|
239
344
|
```
|
|
240
345
|
|
|
346
|
+
## Session Configuration
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
const { Session } = require("httpcloak");
|
|
350
|
+
|
|
351
|
+
const session = new Session({
|
|
352
|
+
preset: "chrome-143", // Browser fingerprint preset
|
|
353
|
+
proxy: null, // Proxy URL
|
|
354
|
+
tcpProxy: null, // Separate TCP proxy
|
|
355
|
+
udpProxy: null, // Separate UDP proxy (MASQUE)
|
|
356
|
+
timeout: 30, // Request timeout in seconds
|
|
357
|
+
httpVersion: "auto", // "auto", "h1", "h2", "h3"
|
|
358
|
+
verify: true, // SSL certificate verification
|
|
359
|
+
allowRedirects: true, // Follow redirects
|
|
360
|
+
maxRedirects: 10, // Maximum redirect count
|
|
361
|
+
retry: 3, // Retry count on failure
|
|
362
|
+
preferIpv4: false, // Prefer IPv4 over IPv6
|
|
363
|
+
connectTo: null, // Domain fronting map
|
|
364
|
+
echConfigDomain: null, // ECH config domain
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
241
368
|
## Available Presets
|
|
242
369
|
|
|
243
370
|
```javascript
|
|
@@ -250,28 +377,90 @@ console.log(availablePresets());
|
|
|
250
377
|
|
|
251
378
|
## Response Object
|
|
252
379
|
|
|
380
|
+
### Standard Response
|
|
381
|
+
|
|
253
382
|
```javascript
|
|
254
383
|
const response = await session.get("https://example.com");
|
|
255
384
|
|
|
256
|
-
response.statusCode;
|
|
257
|
-
response.headers;
|
|
258
|
-
response.body;
|
|
259
|
-
response.text;
|
|
260
|
-
response.finalUrl;
|
|
261
|
-
response.protocol;
|
|
262
|
-
response.
|
|
385
|
+
response.statusCode; // number: HTTP status code
|
|
386
|
+
response.headers; // object: Response headers (values are arrays)
|
|
387
|
+
response.body; // Buffer: Raw response body
|
|
388
|
+
response.text; // string: Response body as text
|
|
389
|
+
response.finalUrl; // string: Final URL after redirects
|
|
390
|
+
response.protocol; // string: Protocol used (http/1.1, h2, h3)
|
|
391
|
+
response.ok; // boolean: True if status < 400
|
|
392
|
+
response.cookies; // array: Cookies from response
|
|
393
|
+
response.history; // array: Redirect history
|
|
394
|
+
|
|
395
|
+
// Get specific header
|
|
396
|
+
const contentType = response.getHeader("Content-Type");
|
|
397
|
+
const allCookies = response.getHeaders("Set-Cookie");
|
|
398
|
+
|
|
399
|
+
// Parse JSON
|
|
400
|
+
const data = response.json();
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Streaming Response
|
|
404
|
+
|
|
405
|
+
```javascript
|
|
406
|
+
const stream = session.getStream("https://example.com");
|
|
407
|
+
|
|
408
|
+
stream.statusCode; // number: HTTP status code
|
|
409
|
+
stream.headers; // object: Response headers (values are arrays)
|
|
410
|
+
stream.contentLength; // number: Content length (-1 if unknown)
|
|
411
|
+
stream.finalUrl; // string: Final URL after redirects
|
|
412
|
+
stream.protocol; // string: Protocol used
|
|
413
|
+
|
|
414
|
+
// Read all bytes
|
|
415
|
+
const data = stream.readAll();
|
|
416
|
+
|
|
417
|
+
// Read in chunks (memory efficient)
|
|
418
|
+
let chunk;
|
|
419
|
+
while ((chunk = stream.readChunk(65536)) !== null) {
|
|
420
|
+
process(chunk);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
stream.close();
|
|
263
424
|
```
|
|
264
425
|
|
|
265
|
-
##
|
|
426
|
+
## HTTP Methods
|
|
266
427
|
|
|
267
428
|
```javascript
|
|
268
|
-
const
|
|
429
|
+
const { Session } = require("httpcloak");
|
|
430
|
+
|
|
431
|
+
const session = new Session({ preset: "chrome-143" });
|
|
432
|
+
|
|
433
|
+
// GET
|
|
434
|
+
const response = await session.get("https://example.com");
|
|
435
|
+
|
|
436
|
+
// POST
|
|
437
|
+
const postResponse = await session.post("https://example.com", { key: "value" });
|
|
438
|
+
|
|
439
|
+
// PUT
|
|
440
|
+
const putResponse = await session.put("https://example.com", { key: "value" });
|
|
441
|
+
|
|
442
|
+
// PATCH
|
|
443
|
+
const patchResponse = await session.patch("https://example.com", { key: "value" });
|
|
444
|
+
|
|
445
|
+
// DELETE
|
|
446
|
+
const deleteResponse = await session.delete("https://example.com");
|
|
447
|
+
|
|
448
|
+
// HEAD
|
|
449
|
+
const headResponse = await session.head("https://example.com");
|
|
450
|
+
|
|
451
|
+
// OPTIONS
|
|
452
|
+
const optionsResponse = await session.options("https://example.com");
|
|
453
|
+
|
|
454
|
+
// Custom request
|
|
455
|
+
const customResponse = await session.request({
|
|
269
456
|
method: "PUT",
|
|
270
457
|
url: "https://api.example.com/resource",
|
|
271
458
|
headers: { "X-Custom": "value" },
|
|
272
459
|
body: { data: "value" },
|
|
273
460
|
timeout: 60,
|
|
274
461
|
});
|
|
462
|
+
|
|
463
|
+
session.close();
|
|
275
464
|
```
|
|
276
465
|
|
|
277
466
|
## Error Handling
|
|
@@ -299,13 +488,22 @@ session.close();
|
|
|
299
488
|
HTTPCloak includes TypeScript definitions out of the box:
|
|
300
489
|
|
|
301
490
|
```typescript
|
|
302
|
-
import { Session, Response, HTTPCloakError } from "httpcloak";
|
|
491
|
+
import { Session, Response, StreamResponse, HTTPCloakError } from "httpcloak";
|
|
303
492
|
|
|
304
493
|
const session = new Session({ preset: "chrome-143" });
|
|
305
494
|
|
|
306
495
|
async function fetchData(): Promise<Response> {
|
|
307
496
|
return session.get("https://example.com");
|
|
308
497
|
}
|
|
498
|
+
|
|
499
|
+
async function downloadLargeFile(): Promise<void> {
|
|
500
|
+
const stream: StreamResponse = session.getStream("https://example.com/file");
|
|
501
|
+
let chunk: Buffer | null;
|
|
502
|
+
while ((chunk = stream.readChunk(65536)) !== null) {
|
|
503
|
+
// Process chunk
|
|
504
|
+
}
|
|
505
|
+
stream.close();
|
|
506
|
+
}
|
|
309
507
|
```
|
|
310
508
|
|
|
311
509
|
## Platform Support
|
|
@@ -313,6 +511,7 @@ async function fetchData(): Promise<Response> {
|
|
|
313
511
|
- Linux (x64, arm64)
|
|
314
512
|
- macOS (x64, arm64)
|
|
315
513
|
- Windows (x64, arm64)
|
|
514
|
+
- Node.js 16+
|
|
316
515
|
|
|
317
516
|
## License
|
|
318
517
|
|
package/lib/index.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ export interface SessionOptions {
|
|
|
64
64
|
preset?: string;
|
|
65
65
|
/** Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port") */
|
|
66
66
|
proxy?: string;
|
|
67
|
+
/** Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config */
|
|
68
|
+
tcpProxy?: string;
|
|
69
|
+
/** Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config */
|
|
70
|
+
udpProxy?: string;
|
|
67
71
|
/** Request timeout in seconds (default: 30) */
|
|
68
72
|
timeout?: number;
|
|
69
73
|
/** HTTP version: "auto", "h1", "h2", "h3" (default: "auto") */
|
package/lib/index.js
CHANGED
|
@@ -235,6 +235,364 @@ class Response {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* High-performance buffer pool using SharedArrayBuffer for zero-allocation copies.
|
|
240
|
+
* Pre-allocates a large buffer once and reuses it across requests.
|
|
241
|
+
*/
|
|
242
|
+
class FastBufferPool {
|
|
243
|
+
constructor() {
|
|
244
|
+
// Pre-allocate 256MB SharedArrayBuffer for maximum performance
|
|
245
|
+
this._sharedBuffer = new SharedArrayBuffer(256 * 1024 * 1024);
|
|
246
|
+
this._bufferView = Buffer.from(this._sharedBuffer);
|
|
247
|
+
this._inUse = false;
|
|
248
|
+
|
|
249
|
+
// Fallback pool for concurrent requests or very large files
|
|
250
|
+
this._fallbackPools = new Map();
|
|
251
|
+
this._tiers = [1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 134217728];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get a buffer of at least the requested size
|
|
256
|
+
* @param {number} size - Minimum buffer size needed
|
|
257
|
+
* @returns {Buffer} - A buffer (may be larger than requested)
|
|
258
|
+
*/
|
|
259
|
+
acquire(size) {
|
|
260
|
+
// Use pre-allocated SharedArrayBuffer if available and large enough
|
|
261
|
+
if (!this._inUse && size <= this._sharedBuffer.byteLength) {
|
|
262
|
+
this._inUse = true;
|
|
263
|
+
return this._bufferView;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fallback to regular buffer pool for concurrent requests
|
|
267
|
+
let tier = this._tiers[this._tiers.length - 1];
|
|
268
|
+
for (const t of this._tiers) {
|
|
269
|
+
if (t >= size) {
|
|
270
|
+
tier = t;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const pool = this._fallbackPools.get(tier);
|
|
276
|
+
if (pool && pool.length > 0) {
|
|
277
|
+
return pool.pop();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return Buffer.allocUnsafe(Math.max(tier, size));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Return a buffer to the pool for reuse
|
|
285
|
+
* @param {Buffer} buffer - Buffer to return
|
|
286
|
+
*/
|
|
287
|
+
release(buffer) {
|
|
288
|
+
// Check if this is our shared buffer
|
|
289
|
+
if (buffer.buffer === this._sharedBuffer) {
|
|
290
|
+
this._inUse = false;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Otherwise add to fallback pool
|
|
295
|
+
const size = buffer.length;
|
|
296
|
+
if (!this._tiers.includes(size)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let pool = this._fallbackPools.get(size);
|
|
301
|
+
if (!pool) {
|
|
302
|
+
pool = [];
|
|
303
|
+
this._fallbackPools.set(size, pool);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (pool.length < 2) {
|
|
307
|
+
pool.push(buffer);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Global buffer pool instance
|
|
313
|
+
const _bufferPool = new FastBufferPool();
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fast response object with zero-copy buffer transfer.
|
|
317
|
+
*
|
|
318
|
+
* This response type avoids JSON serialization and base64 encoding for the body,
|
|
319
|
+
* copying data directly from Go's memory to a Node.js Buffer.
|
|
320
|
+
*
|
|
321
|
+
* Use session.getFast() for maximum download performance.
|
|
322
|
+
*/
|
|
323
|
+
class FastResponse {
|
|
324
|
+
/**
|
|
325
|
+
* @param {Object} metadata - Response metadata from native library
|
|
326
|
+
* @param {Buffer} body - Response body as Buffer (view of pooled buffer)
|
|
327
|
+
* @param {number} [elapsed=0] - Elapsed time in milliseconds
|
|
328
|
+
* @param {Buffer} [pooledBuffer=null] - The underlying pooled buffer for release
|
|
329
|
+
*/
|
|
330
|
+
constructor(metadata, body, elapsed = 0, pooledBuffer = null) {
|
|
331
|
+
this.statusCode = metadata.status_code || 0;
|
|
332
|
+
this.headers = metadata.headers || {};
|
|
333
|
+
this._body = body;
|
|
334
|
+
this._pooledBuffer = pooledBuffer;
|
|
335
|
+
this.finalUrl = metadata.final_url || "";
|
|
336
|
+
this.protocol = metadata.protocol || "";
|
|
337
|
+
this.elapsed = elapsed;
|
|
338
|
+
|
|
339
|
+
// Parse cookies from response
|
|
340
|
+
this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
|
|
341
|
+
|
|
342
|
+
// Parse redirect history
|
|
343
|
+
this._history = (metadata.history || []).map(h => new RedirectInfo(
|
|
344
|
+
h.status_code || 0,
|
|
345
|
+
h.url || "",
|
|
346
|
+
h.headers || {}
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Release the underlying buffer back to the pool.
|
|
352
|
+
* Call this when done with the response to enable buffer reuse.
|
|
353
|
+
* After calling release(), the body buffer should not be used.
|
|
354
|
+
*/
|
|
355
|
+
release() {
|
|
356
|
+
if (this._pooledBuffer) {
|
|
357
|
+
_bufferPool.release(this._pooledBuffer);
|
|
358
|
+
this._pooledBuffer = null;
|
|
359
|
+
this._body = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Cookies set by this response */
|
|
364
|
+
get cookies() {
|
|
365
|
+
return this._cookies;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Redirect history (list of RedirectInfo objects) */
|
|
369
|
+
get history() {
|
|
370
|
+
return this._history;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Response body as string */
|
|
374
|
+
get text() {
|
|
375
|
+
return this._body.toString("utf8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Response body as Buffer */
|
|
379
|
+
get body() {
|
|
380
|
+
return this._body;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Response body as Buffer (requests compatibility alias) */
|
|
384
|
+
get content() {
|
|
385
|
+
return this._body;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Final URL after redirects (requests compatibility alias) */
|
|
389
|
+
get url() {
|
|
390
|
+
return this.finalUrl;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** True if status code < 400 (requests compatibility) */
|
|
394
|
+
get ok() {
|
|
395
|
+
return this.statusCode < 400;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
|
|
399
|
+
get reason() {
|
|
400
|
+
return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Response encoding from Content-Type header.
|
|
405
|
+
* Returns null if not specified.
|
|
406
|
+
*/
|
|
407
|
+
get encoding() {
|
|
408
|
+
let contentType = this.headers["content-type"] || this.headers["Content-Type"] || "";
|
|
409
|
+
if (contentType.includes("charset=")) {
|
|
410
|
+
const parts = contentType.split(";");
|
|
411
|
+
for (const part of parts) {
|
|
412
|
+
const trimmed = part.trim();
|
|
413
|
+
if (trimmed.toLowerCase().startsWith("charset=")) {
|
|
414
|
+
return trimmed.split("=")[1].trim().replace(/['"]/g, "");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Parse response body as JSON
|
|
423
|
+
*/
|
|
424
|
+
json() {
|
|
425
|
+
return JSON.parse(this._body.toString("utf8"));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Raise error if status >= 400 (requests compatibility)
|
|
430
|
+
*/
|
|
431
|
+
raiseForStatus() {
|
|
432
|
+
if (!this.ok) {
|
|
433
|
+
throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Streaming HTTP Response for downloading large files.
|
|
440
|
+
*
|
|
441
|
+
* Example:
|
|
442
|
+
* const stream = session.getStream(url);
|
|
443
|
+
* for await (const chunk of stream) {
|
|
444
|
+
* file.write(chunk);
|
|
445
|
+
* }
|
|
446
|
+
* stream.close();
|
|
447
|
+
*/
|
|
448
|
+
class StreamResponse {
|
|
449
|
+
/**
|
|
450
|
+
* @param {number} streamHandle - Native stream handle
|
|
451
|
+
* @param {Object} lib - Native library
|
|
452
|
+
* @param {Object} metadata - Stream metadata
|
|
453
|
+
*/
|
|
454
|
+
constructor(streamHandle, lib, metadata) {
|
|
455
|
+
this._handle = streamHandle;
|
|
456
|
+
this._lib = lib;
|
|
457
|
+
this.statusCode = metadata.status_code || 0;
|
|
458
|
+
this.headers = metadata.headers || {};
|
|
459
|
+
this.finalUrl = metadata.final_url || "";
|
|
460
|
+
this.protocol = metadata.protocol || "";
|
|
461
|
+
this.contentLength = metadata.content_length || -1;
|
|
462
|
+
this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
|
|
463
|
+
this._closed = false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Cookies set by this response */
|
|
467
|
+
get cookies() {
|
|
468
|
+
return this._cookies;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Final URL after redirects */
|
|
472
|
+
get url() {
|
|
473
|
+
return this.finalUrl;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** True if status code < 400 */
|
|
477
|
+
get ok() {
|
|
478
|
+
return this.statusCode < 400;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** HTTP status reason phrase */
|
|
482
|
+
get reason() {
|
|
483
|
+
return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Read a chunk of data from the stream.
|
|
488
|
+
* @param {number} [chunkSize=8192] - Maximum bytes to read
|
|
489
|
+
* @returns {Buffer|null} - Chunk of data or null if EOF
|
|
490
|
+
*/
|
|
491
|
+
readChunk(chunkSize = 8192) {
|
|
492
|
+
if (this._closed) {
|
|
493
|
+
throw new HTTPCloakError("Stream is closed");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const result = this._lib.httpcloak_stream_read(this._handle, chunkSize);
|
|
497
|
+
if (!result || result === "") {
|
|
498
|
+
return null; // EOF
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Decode base64 to Buffer
|
|
502
|
+
return Buffer.from(result, "base64");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Async generator for iterating over chunks.
|
|
507
|
+
* @param {number} [chunkSize=8192] - Size of each chunk
|
|
508
|
+
* @yields {Buffer} - Chunks of response content
|
|
509
|
+
*
|
|
510
|
+
* Example:
|
|
511
|
+
* for await (const chunk of stream.iterate()) {
|
|
512
|
+
* file.write(chunk);
|
|
513
|
+
* }
|
|
514
|
+
*/
|
|
515
|
+
async *iterate(chunkSize = 8192) {
|
|
516
|
+
while (true) {
|
|
517
|
+
const chunk = this.readChunk(chunkSize);
|
|
518
|
+
if (!chunk) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
yield chunk;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Symbol.asyncIterator for for-await-of loops.
|
|
527
|
+
* @yields {Buffer} - Chunks of response content
|
|
528
|
+
*
|
|
529
|
+
* Example:
|
|
530
|
+
* for await (const chunk of stream) {
|
|
531
|
+
* file.write(chunk);
|
|
532
|
+
* }
|
|
533
|
+
*/
|
|
534
|
+
[Symbol.asyncIterator]() {
|
|
535
|
+
return this.iterate();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read the entire response body as Buffer.
|
|
540
|
+
* Warning: This defeats the purpose of streaming for large files.
|
|
541
|
+
* @returns {Buffer}
|
|
542
|
+
*/
|
|
543
|
+
readAll() {
|
|
544
|
+
const chunks = [];
|
|
545
|
+
let chunk;
|
|
546
|
+
while ((chunk = this.readChunk()) !== null) {
|
|
547
|
+
chunks.push(chunk);
|
|
548
|
+
}
|
|
549
|
+
return Buffer.concat(chunks);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Read the entire response body as string.
|
|
554
|
+
* @returns {string}
|
|
555
|
+
*/
|
|
556
|
+
get text() {
|
|
557
|
+
return this.readAll().toString("utf8");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Read the entire response body as Buffer.
|
|
562
|
+
* @returns {Buffer}
|
|
563
|
+
*/
|
|
564
|
+
get body() {
|
|
565
|
+
return this.readAll();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Parse the response body as JSON.
|
|
570
|
+
* @returns {any}
|
|
571
|
+
*/
|
|
572
|
+
json() {
|
|
573
|
+
return JSON.parse(this.text);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Close the stream and release resources.
|
|
578
|
+
*/
|
|
579
|
+
close() {
|
|
580
|
+
if (!this._closed) {
|
|
581
|
+
this._lib.httpcloak_stream_close(this._handle);
|
|
582
|
+
this._closed = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Raise error if status >= 400
|
|
588
|
+
*/
|
|
589
|
+
raiseForStatus() {
|
|
590
|
+
if (!this.ok) {
|
|
591
|
+
throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
238
596
|
/**
|
|
239
597
|
* Get the platform package name for the current platform
|
|
240
598
|
*/
|
|
@@ -357,6 +715,22 @@ function getLib() {
|
|
|
357
715
|
httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
|
|
358
716
|
httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
|
|
359
717
|
httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
|
|
718
|
+
// Streaming functions
|
|
719
|
+
httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
|
|
720
|
+
httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
|
|
721
|
+
httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
|
|
722
|
+
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", "str", ["int64"]),
|
|
723
|
+
httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", "str", ["int64", "int64"]),
|
|
724
|
+
httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
|
|
725
|
+
// Raw response functions for fast-path (zero-copy)
|
|
726
|
+
httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
|
|
727
|
+
httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
|
|
728
|
+
httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", "str", ["int64"]),
|
|
729
|
+
httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
|
|
730
|
+
httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
|
|
731
|
+
httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
|
|
732
|
+
// Combined finalize function (copy + metadata + free in one call)
|
|
733
|
+
httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", "str", ["int64", "void*", "int"]),
|
|
360
734
|
};
|
|
361
735
|
}
|
|
362
736
|
return lib;
|
|
@@ -675,6 +1049,8 @@ class Session {
|
|
|
675
1049
|
* @param {Object} options - Session options
|
|
676
1050
|
* @param {string} [options.preset="chrome-143"] - Browser preset to use
|
|
677
1051
|
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
|
|
1052
|
+
* @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
|
|
1053
|
+
* @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
|
|
678
1054
|
* @param {number} [options.timeout=30] - Request timeout in seconds
|
|
679
1055
|
* @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
|
|
680
1056
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
@@ -690,6 +1066,8 @@ class Session {
|
|
|
690
1066
|
const {
|
|
691
1067
|
preset = "chrome-143",
|
|
692
1068
|
proxy = null,
|
|
1069
|
+
tcpProxy = null,
|
|
1070
|
+
udpProxy = null,
|
|
693
1071
|
timeout = 30,
|
|
694
1072
|
httpVersion = "auto",
|
|
695
1073
|
verify = true,
|
|
@@ -715,6 +1093,12 @@ class Session {
|
|
|
715
1093
|
if (proxy) {
|
|
716
1094
|
config.proxy = proxy;
|
|
717
1095
|
}
|
|
1096
|
+
if (tcpProxy) {
|
|
1097
|
+
config.tcp_proxy = tcpProxy;
|
|
1098
|
+
}
|
|
1099
|
+
if (udpProxy) {
|
|
1100
|
+
config.udp_proxy = udpProxy;
|
|
1101
|
+
}
|
|
718
1102
|
if (!verify) {
|
|
719
1103
|
config.verify = false;
|
|
720
1104
|
}
|
|
@@ -814,9 +1198,15 @@ class Session {
|
|
|
814
1198
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
815
1199
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
816
1200
|
|
|
817
|
-
|
|
1201
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1202
|
+
const reqOptions = {};
|
|
1203
|
+
if (mergedHeaders) {
|
|
1204
|
+
reqOptions.headers = mergedHeaders;
|
|
1205
|
+
}
|
|
1206
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1207
|
+
|
|
818
1208
|
const startTime = Date.now();
|
|
819
|
-
const result = this._lib.httpcloak_get(this._handle, url,
|
|
1209
|
+
const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
|
|
820
1210
|
const elapsed = Date.now() - startTime;
|
|
821
1211
|
return parseResponse(result, elapsed);
|
|
822
1212
|
}
|
|
@@ -878,9 +1268,15 @@ class Session {
|
|
|
878
1268
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
879
1269
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
880
1270
|
|
|
881
|
-
|
|
1271
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1272
|
+
const reqOptions = {};
|
|
1273
|
+
if (mergedHeaders) {
|
|
1274
|
+
reqOptions.headers = mergedHeaders;
|
|
1275
|
+
}
|
|
1276
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1277
|
+
|
|
882
1278
|
const startTime = Date.now();
|
|
883
|
-
const result = this._lib.httpcloak_post(this._handle, url, body,
|
|
1279
|
+
const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
|
|
884
1280
|
const elapsed = Date.now() - startTime;
|
|
885
1281
|
return parseResponse(result, elapsed);
|
|
886
1282
|
}
|
|
@@ -972,14 +1368,19 @@ class Session {
|
|
|
972
1368
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
973
1369
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
974
1370
|
|
|
975
|
-
|
|
1371
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1372
|
+
const reqOptions = {};
|
|
1373
|
+
if (mergedHeaders) {
|
|
1374
|
+
reqOptions.headers = mergedHeaders;
|
|
1375
|
+
}
|
|
1376
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
976
1377
|
|
|
977
1378
|
// Register async request with callback manager
|
|
978
1379
|
const manager = getAsyncManager();
|
|
979
1380
|
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
980
1381
|
|
|
981
1382
|
// Start async request
|
|
982
|
-
this._lib.httpcloak_get_async(this._handle, url,
|
|
1383
|
+
this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
|
|
983
1384
|
|
|
984
1385
|
return promise;
|
|
985
1386
|
}
|
|
@@ -1031,14 +1432,19 @@ class Session {
|
|
|
1031
1432
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1032
1433
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1033
1434
|
|
|
1034
|
-
|
|
1435
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1436
|
+
const reqOptions = {};
|
|
1437
|
+
if (mergedHeaders) {
|
|
1438
|
+
reqOptions.headers = mergedHeaders;
|
|
1439
|
+
}
|
|
1440
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1035
1441
|
|
|
1036
1442
|
// Register async request with callback manager
|
|
1037
1443
|
const manager = getAsyncManager();
|
|
1038
1444
|
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1039
1445
|
|
|
1040
1446
|
// Start async request
|
|
1041
|
-
this._lib.httpcloak_post_async(this._handle, url, body,
|
|
1447
|
+
this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
|
|
1042
1448
|
|
|
1043
1449
|
return promise;
|
|
1044
1450
|
}
|
|
@@ -1205,6 +1611,394 @@ class Session {
|
|
|
1205
1611
|
get cookies() {
|
|
1206
1612
|
return this.getCookies();
|
|
1207
1613
|
}
|
|
1614
|
+
|
|
1615
|
+
// ===========================================================================
|
|
1616
|
+
// Streaming Methods
|
|
1617
|
+
// ===========================================================================
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Perform a streaming GET request.
|
|
1621
|
+
*
|
|
1622
|
+
* @param {string} url - Request URL
|
|
1623
|
+
* @param {Object} [options] - Request options
|
|
1624
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1625
|
+
* @param {Object} [options.headers] - Request headers
|
|
1626
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1627
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
1628
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1629
|
+
*
|
|
1630
|
+
* Example:
|
|
1631
|
+
* const stream = session.getStream("https://example.com/large-file.zip");
|
|
1632
|
+
* for await (const chunk of stream) {
|
|
1633
|
+
* file.write(chunk);
|
|
1634
|
+
* }
|
|
1635
|
+
* stream.close();
|
|
1636
|
+
*/
|
|
1637
|
+
getStream(url, options = {}) {
|
|
1638
|
+
const { params, headers, cookies, timeout } = options;
|
|
1639
|
+
|
|
1640
|
+
// Add params to URL
|
|
1641
|
+
if (params) {
|
|
1642
|
+
url = addParamsToUrl(url, params);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Merge headers
|
|
1646
|
+
let mergedHeaders = { ...this.headers };
|
|
1647
|
+
if (headers) {
|
|
1648
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1649
|
+
}
|
|
1650
|
+
if (cookies) {
|
|
1651
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1652
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1653
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1654
|
+
: cookieStr;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Build options JSON
|
|
1658
|
+
const reqOptions = {};
|
|
1659
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1660
|
+
reqOptions.headers = mergedHeaders;
|
|
1661
|
+
}
|
|
1662
|
+
if (timeout) {
|
|
1663
|
+
reqOptions.timeout = timeout;
|
|
1664
|
+
}
|
|
1665
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1666
|
+
|
|
1667
|
+
// Start stream
|
|
1668
|
+
const streamHandle = this._lib.httpcloak_stream_get(this._handle, url, optionsJson);
|
|
1669
|
+
if (streamHandle < 0) {
|
|
1670
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Get metadata
|
|
1674
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1675
|
+
if (!metadataStr) {
|
|
1676
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1677
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const metadata = JSON.parse(metadataStr);
|
|
1681
|
+
if (metadata.error) {
|
|
1682
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1683
|
+
throw new HTTPCloakError(metadata.error);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Perform a streaming POST request.
|
|
1691
|
+
*
|
|
1692
|
+
* @param {string} url - Request URL
|
|
1693
|
+
* @param {Object} [options] - Request options
|
|
1694
|
+
* @param {string|Buffer|Object} [options.body] - Request body
|
|
1695
|
+
* @param {Object} [options.json] - JSON body (will be serialized)
|
|
1696
|
+
* @param {Object} [options.form] - Form data (will be URL-encoded)
|
|
1697
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1698
|
+
* @param {Object} [options.headers] - Request headers
|
|
1699
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1700
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
1701
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1702
|
+
*/
|
|
1703
|
+
postStream(url, options = {}) {
|
|
1704
|
+
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
|
|
1705
|
+
|
|
1706
|
+
// Add params to URL
|
|
1707
|
+
if (params) {
|
|
1708
|
+
url = addParamsToUrl(url, params);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Merge headers
|
|
1712
|
+
let mergedHeaders = { ...this.headers };
|
|
1713
|
+
if (headers) {
|
|
1714
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Process body
|
|
1718
|
+
let body = null;
|
|
1719
|
+
if (jsonBody) {
|
|
1720
|
+
body = JSON.stringify(jsonBody);
|
|
1721
|
+
mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/json";
|
|
1722
|
+
} else if (form) {
|
|
1723
|
+
body = Object.entries(form).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
1724
|
+
mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/x-www-form-urlencoded";
|
|
1725
|
+
} else if (bodyOpt) {
|
|
1726
|
+
body = typeof bodyOpt === "string" ? bodyOpt : bodyOpt.toString();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (cookies) {
|
|
1730
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1731
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1732
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1733
|
+
: cookieStr;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Build options JSON
|
|
1737
|
+
const reqOptions = {};
|
|
1738
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1739
|
+
reqOptions.headers = mergedHeaders;
|
|
1740
|
+
}
|
|
1741
|
+
if (timeout) {
|
|
1742
|
+
reqOptions.timeout = timeout;
|
|
1743
|
+
}
|
|
1744
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1745
|
+
|
|
1746
|
+
// Start stream
|
|
1747
|
+
const streamHandle = this._lib.httpcloak_stream_post(this._handle, url, body, optionsJson);
|
|
1748
|
+
if (streamHandle < 0) {
|
|
1749
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Get metadata
|
|
1753
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1754
|
+
if (!metadataStr) {
|
|
1755
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1756
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const metadata = JSON.parse(metadataStr);
|
|
1760
|
+
if (metadata.error) {
|
|
1761
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1762
|
+
throw new HTTPCloakError(metadata.error);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Perform a streaming request with any HTTP method.
|
|
1770
|
+
*
|
|
1771
|
+
* @param {string} method - HTTP method
|
|
1772
|
+
* @param {string} url - Request URL
|
|
1773
|
+
* @param {Object} [options] - Request options
|
|
1774
|
+
* @param {string|Buffer} [options.body] - Request body
|
|
1775
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1776
|
+
* @param {Object} [options.headers] - Request headers
|
|
1777
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1778
|
+
* @param {number} [options.timeout] - Request timeout in seconds
|
|
1779
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1780
|
+
*/
|
|
1781
|
+
requestStream(method, url, options = {}) {
|
|
1782
|
+
const { body, params, headers, cookies, timeout } = options;
|
|
1783
|
+
|
|
1784
|
+
// Add params to URL
|
|
1785
|
+
if (params) {
|
|
1786
|
+
url = addParamsToUrl(url, params);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Merge headers
|
|
1790
|
+
let mergedHeaders = { ...this.headers };
|
|
1791
|
+
if (headers) {
|
|
1792
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1793
|
+
}
|
|
1794
|
+
if (cookies) {
|
|
1795
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1796
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1797
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1798
|
+
: cookieStr;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Build request config
|
|
1802
|
+
const requestConfig = {
|
|
1803
|
+
method: method.toUpperCase(),
|
|
1804
|
+
url,
|
|
1805
|
+
};
|
|
1806
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1807
|
+
requestConfig.headers = mergedHeaders;
|
|
1808
|
+
}
|
|
1809
|
+
if (body) {
|
|
1810
|
+
requestConfig.body = typeof body === "string" ? body : body.toString();
|
|
1811
|
+
}
|
|
1812
|
+
if (timeout) {
|
|
1813
|
+
requestConfig.timeout = timeout;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Start stream
|
|
1817
|
+
const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
|
|
1818
|
+
if (streamHandle < 0) {
|
|
1819
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Get metadata
|
|
1823
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1824
|
+
if (!metadataStr) {
|
|
1825
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1826
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const metadata = JSON.parse(metadataStr);
|
|
1830
|
+
if (metadata.error) {
|
|
1831
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1832
|
+
throw new HTTPCloakError(metadata.error);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// ===========================================================================
|
|
1839
|
+
// Fast-path Methods (Zero-copy for maximum performance)
|
|
1840
|
+
// ===========================================================================
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Perform a fast GET request with zero-copy buffer transfer.
|
|
1844
|
+
*
|
|
1845
|
+
* This method bypasses JSON serialization and base64 encoding for the response body,
|
|
1846
|
+
* copying data directly from Go's memory to a Node.js Buffer.
|
|
1847
|
+
*
|
|
1848
|
+
* Use this method for downloading large files when you need maximum throughput.
|
|
1849
|
+
*
|
|
1850
|
+
* @param {string} url - Request URL
|
|
1851
|
+
* @param {Object} [options] - Request options
|
|
1852
|
+
* @param {Object} [options.headers] - Custom headers
|
|
1853
|
+
* @param {Object} [options.params] - Query parameters
|
|
1854
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
1855
|
+
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
1856
|
+
* @returns {FastResponse} Fast response object with Buffer body
|
|
1857
|
+
*
|
|
1858
|
+
* Example:
|
|
1859
|
+
* const response = session.getFast("https://example.com/large-file.zip");
|
|
1860
|
+
* console.log(`Downloaded ${response.body.length} bytes`);
|
|
1861
|
+
* fs.writeFileSync("file.zip", response.body);
|
|
1862
|
+
*/
|
|
1863
|
+
getFast(url, options = {}) {
|
|
1864
|
+
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1865
|
+
|
|
1866
|
+
url = addParamsToUrl(url, params);
|
|
1867
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
1868
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1869
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1870
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1871
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1872
|
+
|
|
1873
|
+
// Build request options JSON with headers wrapper
|
|
1874
|
+
const reqOptions = {};
|
|
1875
|
+
if (mergedHeaders) {
|
|
1876
|
+
reqOptions.headers = mergedHeaders;
|
|
1877
|
+
}
|
|
1878
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1879
|
+
|
|
1880
|
+
const startTime = Date.now();
|
|
1881
|
+
|
|
1882
|
+
// Get raw response handle
|
|
1883
|
+
const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
|
|
1884
|
+
if (responseHandle === 0 || responseHandle === 0n) {
|
|
1885
|
+
throw new HTTPCloakError("Failed to make request");
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Get body length first (1 FFI call)
|
|
1889
|
+
const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
|
|
1890
|
+
if (bodyLen < 0) {
|
|
1891
|
+
this._lib.httpcloak_response_free(responseHandle);
|
|
1892
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Acquire pooled buffer
|
|
1896
|
+
const pooledBuffer = _bufferPool.acquire(bodyLen);
|
|
1897
|
+
|
|
1898
|
+
// Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
|
|
1899
|
+
const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
|
|
1900
|
+
if (!metadataStr) {
|
|
1901
|
+
_bufferPool.release(pooledBuffer);
|
|
1902
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const metadata = JSON.parse(metadataStr);
|
|
1906
|
+
if (metadata.error) {
|
|
1907
|
+
_bufferPool.release(pooledBuffer);
|
|
1908
|
+
throw new HTTPCloakError(metadata.error);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// Create a view of just the used portion
|
|
1912
|
+
const buffer = pooledBuffer.subarray(0, bodyLen);
|
|
1913
|
+
|
|
1914
|
+
const elapsed = Date.now() - startTime;
|
|
1915
|
+
return new FastResponse(metadata, buffer, elapsed, pooledBuffer);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* High-performance POST request optimized for large uploads.
|
|
1920
|
+
*
|
|
1921
|
+
* Uses binary buffer passing and response pooling for maximum throughput.
|
|
1922
|
+
* Call response.release() when done to return buffers to pool.
|
|
1923
|
+
*
|
|
1924
|
+
* @param {string} url - Request URL
|
|
1925
|
+
* @param {Object} [options] - Request options
|
|
1926
|
+
* @param {Buffer} [options.body] - Request body as Buffer
|
|
1927
|
+
* @param {Object} [options.headers] - Request headers
|
|
1928
|
+
* @param {Object} [options.params] - Query parameters
|
|
1929
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
1930
|
+
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
1931
|
+
* @returns {FastResponse} Fast response object with Buffer body
|
|
1932
|
+
*
|
|
1933
|
+
* Example:
|
|
1934
|
+
* const data = Buffer.alloc(10 * 1024 * 1024); // 10MB
|
|
1935
|
+
* const response = session.postFast("https://example.com/upload", { body: data });
|
|
1936
|
+
* console.log(`Uploaded, response: ${response.statusCode}`);
|
|
1937
|
+
* response.release();
|
|
1938
|
+
*/
|
|
1939
|
+
postFast(url, options = {}) {
|
|
1940
|
+
let { body = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
1941
|
+
|
|
1942
|
+
url = addParamsToUrl(url, params);
|
|
1943
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
1944
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1945
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1946
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1947
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1948
|
+
|
|
1949
|
+
// Ensure body is a Buffer
|
|
1950
|
+
if (body === null) {
|
|
1951
|
+
body = Buffer.alloc(0);
|
|
1952
|
+
} else if (typeof body === "string") {
|
|
1953
|
+
body = Buffer.from(body, "utf8");
|
|
1954
|
+
} else if (!Buffer.isBuffer(body)) {
|
|
1955
|
+
throw new HTTPCloakError("postFast body must be a Buffer or string");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Build request options JSON with headers wrapper
|
|
1959
|
+
const reqOptions = {};
|
|
1960
|
+
if (mergedHeaders) {
|
|
1961
|
+
reqOptions.headers = mergedHeaders;
|
|
1962
|
+
}
|
|
1963
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1964
|
+
|
|
1965
|
+
const startTime = Date.now();
|
|
1966
|
+
|
|
1967
|
+
// Use httpcloak_post_raw with binary buffer (no string conversion!)
|
|
1968
|
+
const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, body, body.length, optionsJson);
|
|
1969
|
+
if (responseHandle === 0 || responseHandle === 0n || responseHandle < 0) {
|
|
1970
|
+
throw new HTTPCloakError("Failed to make POST request");
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Get body length first (1 FFI call)
|
|
1974
|
+
const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
|
|
1975
|
+
if (bodyLen < 0) {
|
|
1976
|
+
this._lib.httpcloak_response_free(responseHandle);
|
|
1977
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// Acquire pooled buffer for response
|
|
1981
|
+
const pooledBuffer = _bufferPool.acquire(bodyLen);
|
|
1982
|
+
|
|
1983
|
+
// Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
|
|
1984
|
+
const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
|
|
1985
|
+
if (!metadataStr) {
|
|
1986
|
+
_bufferPool.release(pooledBuffer);
|
|
1987
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
const metadata = JSON.parse(metadataStr);
|
|
1991
|
+
if (metadata.error) {
|
|
1992
|
+
_bufferPool.release(pooledBuffer);
|
|
1993
|
+
throw new HTTPCloakError(metadata.error);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Create a view of just the used portion
|
|
1997
|
+
const responseBuffer = pooledBuffer.subarray(0, bodyLen);
|
|
1998
|
+
|
|
1999
|
+
const elapsed = Date.now() - startTime;
|
|
2000
|
+
return new FastResponse(metadata, responseBuffer, elapsed, pooledBuffer);
|
|
2001
|
+
}
|
|
1208
2002
|
}
|
|
1209
2003
|
|
|
1210
2004
|
// =============================================================================
|
|
@@ -1384,6 +2178,8 @@ module.exports = {
|
|
|
1384
2178
|
// Classes
|
|
1385
2179
|
Session,
|
|
1386
2180
|
Response,
|
|
2181
|
+
FastResponse,
|
|
2182
|
+
StreamResponse,
|
|
1387
2183
|
Cookie,
|
|
1388
2184
|
RedirectInfo,
|
|
1389
2185
|
HTTPCloakError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "httpcloak",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Browser fingerprint emulation HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.mjs",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"koffi": "^2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@httpcloak/linux-x64": "1.5.
|
|
53
|
-
"@httpcloak/linux-arm64": "1.5.
|
|
54
|
-
"@httpcloak/darwin-x64": "1.5.
|
|
55
|
-
"@httpcloak/darwin-arm64": "1.5.
|
|
56
|
-
"@httpcloak/win32-x64": "1.5.
|
|
57
|
-
"@httpcloak/win32-arm64": "1.5.
|
|
52
|
+
"@httpcloak/linux-x64": "1.5.3",
|
|
53
|
+
"@httpcloak/linux-arm64": "1.5.3",
|
|
54
|
+
"@httpcloak/darwin-x64": "1.5.3",
|
|
55
|
+
"@httpcloak/darwin-arm64": "1.5.3",
|
|
56
|
+
"@httpcloak/win32-x64": "1.5.3",
|
|
57
|
+
"@httpcloak/win32-arm64": "1.5.3"
|
|
58
58
|
}
|
|
59
59
|
}
|