httpcloak 1.5.2 → 1.5.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 +212 -13
- package/lib/index.d.ts +4 -0
- package/lib/index.js +930 -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/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,27 @@ 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"]),
|
|
734
|
+
// Session persistence functions
|
|
735
|
+
httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", "str", ["int64", "str"]),
|
|
736
|
+
httpcloak_session_load: nativeLibHandle.func("httpcloak_session_load", "int64", ["str"]),
|
|
737
|
+
httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", "str", ["int64"]),
|
|
738
|
+
httpcloak_session_unmarshal: nativeLibHandle.func("httpcloak_session_unmarshal", "int64", ["str"]),
|
|
360
739
|
};
|
|
361
740
|
}
|
|
362
741
|
return lib;
|
|
@@ -675,6 +1054,8 @@ class Session {
|
|
|
675
1054
|
* @param {Object} options - Session options
|
|
676
1055
|
* @param {string} [options.preset="chrome-143"] - Browser preset to use
|
|
677
1056
|
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
|
|
1057
|
+
* @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
|
|
1058
|
+
* @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
|
|
678
1059
|
* @param {number} [options.timeout=30] - Request timeout in seconds
|
|
679
1060
|
* @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
|
|
680
1061
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
@@ -690,6 +1071,8 @@ class Session {
|
|
|
690
1071
|
const {
|
|
691
1072
|
preset = "chrome-143",
|
|
692
1073
|
proxy = null,
|
|
1074
|
+
tcpProxy = null,
|
|
1075
|
+
udpProxy = null,
|
|
693
1076
|
timeout = 30,
|
|
694
1077
|
httpVersion = "auto",
|
|
695
1078
|
verify = true,
|
|
@@ -715,6 +1098,12 @@ class Session {
|
|
|
715
1098
|
if (proxy) {
|
|
716
1099
|
config.proxy = proxy;
|
|
717
1100
|
}
|
|
1101
|
+
if (tcpProxy) {
|
|
1102
|
+
config.tcp_proxy = tcpProxy;
|
|
1103
|
+
}
|
|
1104
|
+
if (udpProxy) {
|
|
1105
|
+
config.udp_proxy = udpProxy;
|
|
1106
|
+
}
|
|
718
1107
|
if (!verify) {
|
|
719
1108
|
config.verify = false;
|
|
720
1109
|
}
|
|
@@ -814,9 +1203,15 @@ class Session {
|
|
|
814
1203
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
815
1204
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
816
1205
|
|
|
817
|
-
|
|
1206
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1207
|
+
const reqOptions = {};
|
|
1208
|
+
if (mergedHeaders) {
|
|
1209
|
+
reqOptions.headers = mergedHeaders;
|
|
1210
|
+
}
|
|
1211
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1212
|
+
|
|
818
1213
|
const startTime = Date.now();
|
|
819
|
-
const result = this._lib.httpcloak_get(this._handle, url,
|
|
1214
|
+
const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
|
|
820
1215
|
const elapsed = Date.now() - startTime;
|
|
821
1216
|
return parseResponse(result, elapsed);
|
|
822
1217
|
}
|
|
@@ -878,9 +1273,15 @@ class Session {
|
|
|
878
1273
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
879
1274
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
880
1275
|
|
|
881
|
-
|
|
1276
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1277
|
+
const reqOptions = {};
|
|
1278
|
+
if (mergedHeaders) {
|
|
1279
|
+
reqOptions.headers = mergedHeaders;
|
|
1280
|
+
}
|
|
1281
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1282
|
+
|
|
882
1283
|
const startTime = Date.now();
|
|
883
|
-
const result = this._lib.httpcloak_post(this._handle, url, body,
|
|
1284
|
+
const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
|
|
884
1285
|
const elapsed = Date.now() - startTime;
|
|
885
1286
|
return parseResponse(result, elapsed);
|
|
886
1287
|
}
|
|
@@ -972,14 +1373,19 @@ class Session {
|
|
|
972
1373
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
973
1374
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
974
1375
|
|
|
975
|
-
|
|
1376
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1377
|
+
const reqOptions = {};
|
|
1378
|
+
if (mergedHeaders) {
|
|
1379
|
+
reqOptions.headers = mergedHeaders;
|
|
1380
|
+
}
|
|
1381
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
976
1382
|
|
|
977
1383
|
// Register async request with callback manager
|
|
978
1384
|
const manager = getAsyncManager();
|
|
979
1385
|
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
980
1386
|
|
|
981
1387
|
// Start async request
|
|
982
|
-
this._lib.httpcloak_get_async(this._handle, url,
|
|
1388
|
+
this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
|
|
983
1389
|
|
|
984
1390
|
return promise;
|
|
985
1391
|
}
|
|
@@ -1031,14 +1437,19 @@ class Session {
|
|
|
1031
1437
|
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1032
1438
|
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1033
1439
|
|
|
1034
|
-
|
|
1440
|
+
// Build request options JSON with headers wrapper (clib expects {"headers": {...}})
|
|
1441
|
+
const reqOptions = {};
|
|
1442
|
+
if (mergedHeaders) {
|
|
1443
|
+
reqOptions.headers = mergedHeaders;
|
|
1444
|
+
}
|
|
1445
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1035
1446
|
|
|
1036
1447
|
// Register async request with callback manager
|
|
1037
1448
|
const manager = getAsyncManager();
|
|
1038
1449
|
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1039
1450
|
|
|
1040
1451
|
// Start async request
|
|
1041
|
-
this._lib.httpcloak_post_async(this._handle, url, body,
|
|
1452
|
+
this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
|
|
1042
1453
|
|
|
1043
1454
|
return promise;
|
|
1044
1455
|
}
|
|
@@ -1205,6 +1616,515 @@ class Session {
|
|
|
1205
1616
|
get cookies() {
|
|
1206
1617
|
return this.getCookies();
|
|
1207
1618
|
}
|
|
1619
|
+
|
|
1620
|
+
// ===========================================================================
|
|
1621
|
+
// Session Persistence
|
|
1622
|
+
// ===========================================================================
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Save session state (cookies, TLS sessions) to a file.
|
|
1626
|
+
*
|
|
1627
|
+
* This allows you to persist session state across program runs,
|
|
1628
|
+
* including cookies and TLS session tickets for faster resumption.
|
|
1629
|
+
*
|
|
1630
|
+
* @param {string} path - Path to save the session file
|
|
1631
|
+
*
|
|
1632
|
+
* Example:
|
|
1633
|
+
* const session = new httpcloak.Session({ preset: "chrome-143" });
|
|
1634
|
+
* await session.get("https://example.com"); // Acquire cookies
|
|
1635
|
+
* session.save("session.json");
|
|
1636
|
+
*
|
|
1637
|
+
* // Later, restore the session
|
|
1638
|
+
* const session = httpcloak.Session.load("session.json");
|
|
1639
|
+
*/
|
|
1640
|
+
save(path) {
|
|
1641
|
+
const result = this._lib.httpcloak_session_save(this._handle, path);
|
|
1642
|
+
if (result) {
|
|
1643
|
+
const data = JSON.parse(result);
|
|
1644
|
+
if (data.error) {
|
|
1645
|
+
throw new HTTPCloakError(data.error);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Export session state to JSON string.
|
|
1652
|
+
*
|
|
1653
|
+
* @returns {string} JSON string containing session state
|
|
1654
|
+
*
|
|
1655
|
+
* Example:
|
|
1656
|
+
* const sessionData = session.marshal();
|
|
1657
|
+
* // Store sessionData in database, cache, etc.
|
|
1658
|
+
*
|
|
1659
|
+
* // Later, restore the session
|
|
1660
|
+
* const session = httpcloak.Session.unmarshal(sessionData);
|
|
1661
|
+
*/
|
|
1662
|
+
marshal() {
|
|
1663
|
+
const result = this._lib.httpcloak_session_marshal(this._handle);
|
|
1664
|
+
if (!result) {
|
|
1665
|
+
throw new HTTPCloakError("Failed to marshal session");
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Check for error
|
|
1669
|
+
try {
|
|
1670
|
+
const data = JSON.parse(result);
|
|
1671
|
+
if (data && typeof data === "object" && data.error) {
|
|
1672
|
+
throw new HTTPCloakError(data.error);
|
|
1673
|
+
}
|
|
1674
|
+
} catch (e) {
|
|
1675
|
+
if (e instanceof HTTPCloakError) throw e;
|
|
1676
|
+
// Not an error response, just JSON parse failed - return as is
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return result;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Load a session from a file.
|
|
1684
|
+
*
|
|
1685
|
+
* This restores session state including cookies and TLS session tickets.
|
|
1686
|
+
* The session uses the same preset that was used when it was saved.
|
|
1687
|
+
*
|
|
1688
|
+
* @param {string} path - Path to the session file
|
|
1689
|
+
* @returns {Session} Restored Session object
|
|
1690
|
+
*
|
|
1691
|
+
* Example:
|
|
1692
|
+
* const session = httpcloak.Session.load("session.json");
|
|
1693
|
+
* const r = await session.get("https://example.com"); // Uses restored cookies
|
|
1694
|
+
*/
|
|
1695
|
+
static load(path) {
|
|
1696
|
+
const lib = getLib();
|
|
1697
|
+
const handle = lib.httpcloak_session_load(path);
|
|
1698
|
+
|
|
1699
|
+
if (handle < 0 || handle === 0n) {
|
|
1700
|
+
throw new HTTPCloakError(`Failed to load session from ${path}`);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Create a new Session instance without calling constructor
|
|
1704
|
+
const session = Object.create(Session.prototype);
|
|
1705
|
+
session._lib = lib;
|
|
1706
|
+
session._handle = handle;
|
|
1707
|
+
session.headers = {};
|
|
1708
|
+
session.auth = null;
|
|
1709
|
+
|
|
1710
|
+
return session;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Load a session from JSON string.
|
|
1715
|
+
*
|
|
1716
|
+
* @param {string} data - JSON string containing session state
|
|
1717
|
+
* @returns {Session} Restored Session object
|
|
1718
|
+
*
|
|
1719
|
+
* Example:
|
|
1720
|
+
* // Retrieve sessionData from database, cache, etc.
|
|
1721
|
+
* const session = httpcloak.Session.unmarshal(sessionData);
|
|
1722
|
+
*/
|
|
1723
|
+
static unmarshal(data) {
|
|
1724
|
+
const lib = getLib();
|
|
1725
|
+
const handle = lib.httpcloak_session_unmarshal(data);
|
|
1726
|
+
|
|
1727
|
+
if (handle < 0 || handle === 0n) {
|
|
1728
|
+
throw new HTTPCloakError("Failed to unmarshal session");
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Create a new Session instance without calling constructor
|
|
1732
|
+
const session = Object.create(Session.prototype);
|
|
1733
|
+
session._lib = lib;
|
|
1734
|
+
session._handle = handle;
|
|
1735
|
+
session.headers = {};
|
|
1736
|
+
session.auth = null;
|
|
1737
|
+
|
|
1738
|
+
return session;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// ===========================================================================
|
|
1742
|
+
// Streaming Methods
|
|
1743
|
+
// ===========================================================================
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Perform a streaming GET request.
|
|
1747
|
+
*
|
|
1748
|
+
* @param {string} url - Request URL
|
|
1749
|
+
* @param {Object} [options] - Request options
|
|
1750
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1751
|
+
* @param {Object} [options.headers] - Request headers
|
|
1752
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1753
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
1754
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1755
|
+
*
|
|
1756
|
+
* Example:
|
|
1757
|
+
* const stream = session.getStream("https://example.com/large-file.zip");
|
|
1758
|
+
* for await (const chunk of stream) {
|
|
1759
|
+
* file.write(chunk);
|
|
1760
|
+
* }
|
|
1761
|
+
* stream.close();
|
|
1762
|
+
*/
|
|
1763
|
+
getStream(url, options = {}) {
|
|
1764
|
+
const { params, headers, cookies, timeout } = options;
|
|
1765
|
+
|
|
1766
|
+
// Add params to URL
|
|
1767
|
+
if (params) {
|
|
1768
|
+
url = addParamsToUrl(url, params);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Merge headers
|
|
1772
|
+
let mergedHeaders = { ...this.headers };
|
|
1773
|
+
if (headers) {
|
|
1774
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1775
|
+
}
|
|
1776
|
+
if (cookies) {
|
|
1777
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1778
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1779
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1780
|
+
: cookieStr;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Build options JSON
|
|
1784
|
+
const reqOptions = {};
|
|
1785
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1786
|
+
reqOptions.headers = mergedHeaders;
|
|
1787
|
+
}
|
|
1788
|
+
if (timeout) {
|
|
1789
|
+
reqOptions.timeout = timeout;
|
|
1790
|
+
}
|
|
1791
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1792
|
+
|
|
1793
|
+
// Start stream
|
|
1794
|
+
const streamHandle = this._lib.httpcloak_stream_get(this._handle, url, optionsJson);
|
|
1795
|
+
if (streamHandle < 0) {
|
|
1796
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Get metadata
|
|
1800
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1801
|
+
if (!metadataStr) {
|
|
1802
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1803
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const metadata = JSON.parse(metadataStr);
|
|
1807
|
+
if (metadata.error) {
|
|
1808
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1809
|
+
throw new HTTPCloakError(metadata.error);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* Perform a streaming POST request.
|
|
1817
|
+
*
|
|
1818
|
+
* @param {string} url - Request URL
|
|
1819
|
+
* @param {Object} [options] - Request options
|
|
1820
|
+
* @param {string|Buffer|Object} [options.body] - Request body
|
|
1821
|
+
* @param {Object} [options.json] - JSON body (will be serialized)
|
|
1822
|
+
* @param {Object} [options.form] - Form data (will be URL-encoded)
|
|
1823
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1824
|
+
* @param {Object} [options.headers] - Request headers
|
|
1825
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1826
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
1827
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1828
|
+
*/
|
|
1829
|
+
postStream(url, options = {}) {
|
|
1830
|
+
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
|
|
1831
|
+
|
|
1832
|
+
// Add params to URL
|
|
1833
|
+
if (params) {
|
|
1834
|
+
url = addParamsToUrl(url, params);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Merge headers
|
|
1838
|
+
let mergedHeaders = { ...this.headers };
|
|
1839
|
+
if (headers) {
|
|
1840
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Process body
|
|
1844
|
+
let body = null;
|
|
1845
|
+
if (jsonBody) {
|
|
1846
|
+
body = JSON.stringify(jsonBody);
|
|
1847
|
+
mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/json";
|
|
1848
|
+
} else if (form) {
|
|
1849
|
+
body = Object.entries(form).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
1850
|
+
mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/x-www-form-urlencoded";
|
|
1851
|
+
} else if (bodyOpt) {
|
|
1852
|
+
body = typeof bodyOpt === "string" ? bodyOpt : bodyOpt.toString();
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (cookies) {
|
|
1856
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1857
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1858
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1859
|
+
: cookieStr;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Build options JSON
|
|
1863
|
+
const reqOptions = {};
|
|
1864
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1865
|
+
reqOptions.headers = mergedHeaders;
|
|
1866
|
+
}
|
|
1867
|
+
if (timeout) {
|
|
1868
|
+
reqOptions.timeout = timeout;
|
|
1869
|
+
}
|
|
1870
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1871
|
+
|
|
1872
|
+
// Start stream
|
|
1873
|
+
const streamHandle = this._lib.httpcloak_stream_post(this._handle, url, body, optionsJson);
|
|
1874
|
+
if (streamHandle < 0) {
|
|
1875
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Get metadata
|
|
1879
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1880
|
+
if (!metadataStr) {
|
|
1881
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1882
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const metadata = JSON.parse(metadataStr);
|
|
1886
|
+
if (metadata.error) {
|
|
1887
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1888
|
+
throw new HTTPCloakError(metadata.error);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/**
|
|
1895
|
+
* Perform a streaming request with any HTTP method.
|
|
1896
|
+
*
|
|
1897
|
+
* @param {string} method - HTTP method
|
|
1898
|
+
* @param {string} url - Request URL
|
|
1899
|
+
* @param {Object} [options] - Request options
|
|
1900
|
+
* @param {string|Buffer} [options.body] - Request body
|
|
1901
|
+
* @param {Object} [options.params] - URL query parameters
|
|
1902
|
+
* @param {Object} [options.headers] - Request headers
|
|
1903
|
+
* @param {Object} [options.cookies] - Cookies to send
|
|
1904
|
+
* @param {number} [options.timeout] - Request timeout in seconds
|
|
1905
|
+
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
1906
|
+
*/
|
|
1907
|
+
requestStream(method, url, options = {}) {
|
|
1908
|
+
const { body, params, headers, cookies, timeout } = options;
|
|
1909
|
+
|
|
1910
|
+
// Add params to URL
|
|
1911
|
+
if (params) {
|
|
1912
|
+
url = addParamsToUrl(url, params);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// Merge headers
|
|
1916
|
+
let mergedHeaders = { ...this.headers };
|
|
1917
|
+
if (headers) {
|
|
1918
|
+
mergedHeaders = { ...mergedHeaders, ...headers };
|
|
1919
|
+
}
|
|
1920
|
+
if (cookies) {
|
|
1921
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1922
|
+
mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
|
|
1923
|
+
? `${mergedHeaders["Cookie"]}; ${cookieStr}`
|
|
1924
|
+
: cookieStr;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// Build request config
|
|
1928
|
+
const requestConfig = {
|
|
1929
|
+
method: method.toUpperCase(),
|
|
1930
|
+
url,
|
|
1931
|
+
};
|
|
1932
|
+
if (Object.keys(mergedHeaders).length > 0) {
|
|
1933
|
+
requestConfig.headers = mergedHeaders;
|
|
1934
|
+
}
|
|
1935
|
+
if (body) {
|
|
1936
|
+
requestConfig.body = typeof body === "string" ? body : body.toString();
|
|
1937
|
+
}
|
|
1938
|
+
if (timeout) {
|
|
1939
|
+
requestConfig.timeout = timeout;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Start stream
|
|
1943
|
+
const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
|
|
1944
|
+
if (streamHandle < 0) {
|
|
1945
|
+
throw new HTTPCloakError("Failed to start streaming request");
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// Get metadata
|
|
1949
|
+
const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
|
|
1950
|
+
if (!metadataStr) {
|
|
1951
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1952
|
+
throw new HTTPCloakError("Failed to get stream metadata");
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
const metadata = JSON.parse(metadataStr);
|
|
1956
|
+
if (metadata.error) {
|
|
1957
|
+
this._lib.httpcloak_stream_close(streamHandle);
|
|
1958
|
+
throw new HTTPCloakError(metadata.error);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
return new StreamResponse(streamHandle, this._lib, metadata);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// ===========================================================================
|
|
1965
|
+
// Fast-path Methods (Zero-copy for maximum performance)
|
|
1966
|
+
// ===========================================================================
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Perform a fast GET request with zero-copy buffer transfer.
|
|
1970
|
+
*
|
|
1971
|
+
* This method bypasses JSON serialization and base64 encoding for the response body,
|
|
1972
|
+
* copying data directly from Go's memory to a Node.js Buffer.
|
|
1973
|
+
*
|
|
1974
|
+
* Use this method for downloading large files when you need maximum throughput.
|
|
1975
|
+
*
|
|
1976
|
+
* @param {string} url - Request URL
|
|
1977
|
+
* @param {Object} [options] - Request options
|
|
1978
|
+
* @param {Object} [options.headers] - Custom headers
|
|
1979
|
+
* @param {Object} [options.params] - Query parameters
|
|
1980
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
1981
|
+
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
1982
|
+
* @returns {FastResponse} Fast response object with Buffer body
|
|
1983
|
+
*
|
|
1984
|
+
* Example:
|
|
1985
|
+
* const response = session.getFast("https://example.com/large-file.zip");
|
|
1986
|
+
* console.log(`Downloaded ${response.body.length} bytes`);
|
|
1987
|
+
* fs.writeFileSync("file.zip", response.body);
|
|
1988
|
+
*/
|
|
1989
|
+
getFast(url, options = {}) {
|
|
1990
|
+
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1991
|
+
|
|
1992
|
+
url = addParamsToUrl(url, params);
|
|
1993
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
1994
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1995
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1996
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1997
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1998
|
+
|
|
1999
|
+
// Build request options JSON with headers wrapper
|
|
2000
|
+
const reqOptions = {};
|
|
2001
|
+
if (mergedHeaders) {
|
|
2002
|
+
reqOptions.headers = mergedHeaders;
|
|
2003
|
+
}
|
|
2004
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2005
|
+
|
|
2006
|
+
const startTime = Date.now();
|
|
2007
|
+
|
|
2008
|
+
// Get raw response handle
|
|
2009
|
+
const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
|
|
2010
|
+
if (responseHandle === 0 || responseHandle === 0n) {
|
|
2011
|
+
throw new HTTPCloakError("Failed to make request");
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Get body length first (1 FFI call)
|
|
2015
|
+
const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
|
|
2016
|
+
if (bodyLen < 0) {
|
|
2017
|
+
this._lib.httpcloak_response_free(responseHandle);
|
|
2018
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Acquire pooled buffer
|
|
2022
|
+
const pooledBuffer = _bufferPool.acquire(bodyLen);
|
|
2023
|
+
|
|
2024
|
+
// Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
|
|
2025
|
+
const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
|
|
2026
|
+
if (!metadataStr) {
|
|
2027
|
+
_bufferPool.release(pooledBuffer);
|
|
2028
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const metadata = JSON.parse(metadataStr);
|
|
2032
|
+
if (metadata.error) {
|
|
2033
|
+
_bufferPool.release(pooledBuffer);
|
|
2034
|
+
throw new HTTPCloakError(metadata.error);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Create a view of just the used portion
|
|
2038
|
+
const buffer = pooledBuffer.subarray(0, bodyLen);
|
|
2039
|
+
|
|
2040
|
+
const elapsed = Date.now() - startTime;
|
|
2041
|
+
return new FastResponse(metadata, buffer, elapsed, pooledBuffer);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* High-performance POST request optimized for large uploads.
|
|
2046
|
+
*
|
|
2047
|
+
* Uses binary buffer passing and response pooling for maximum throughput.
|
|
2048
|
+
* Call response.release() when done to return buffers to pool.
|
|
2049
|
+
*
|
|
2050
|
+
* @param {string} url - Request URL
|
|
2051
|
+
* @param {Object} [options] - Request options
|
|
2052
|
+
* @param {Buffer} [options.body] - Request body as Buffer
|
|
2053
|
+
* @param {Object} [options.headers] - Request headers
|
|
2054
|
+
* @param {Object} [options.params] - Query parameters
|
|
2055
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
2056
|
+
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
2057
|
+
* @returns {FastResponse} Fast response object with Buffer body
|
|
2058
|
+
*
|
|
2059
|
+
* Example:
|
|
2060
|
+
* const data = Buffer.alloc(10 * 1024 * 1024); // 10MB
|
|
2061
|
+
* const response = session.postFast("https://example.com/upload", { body: data });
|
|
2062
|
+
* console.log(`Uploaded, response: ${response.statusCode}`);
|
|
2063
|
+
* response.release();
|
|
2064
|
+
*/
|
|
2065
|
+
postFast(url, options = {}) {
|
|
2066
|
+
let { body = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
2067
|
+
|
|
2068
|
+
url = addParamsToUrl(url, params);
|
|
2069
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
2070
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
2071
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
2072
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
2073
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
2074
|
+
|
|
2075
|
+
// Ensure body is a Buffer
|
|
2076
|
+
if (body === null) {
|
|
2077
|
+
body = Buffer.alloc(0);
|
|
2078
|
+
} else if (typeof body === "string") {
|
|
2079
|
+
body = Buffer.from(body, "utf8");
|
|
2080
|
+
} else if (!Buffer.isBuffer(body)) {
|
|
2081
|
+
throw new HTTPCloakError("postFast body must be a Buffer or string");
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Build request options JSON with headers wrapper
|
|
2085
|
+
const reqOptions = {};
|
|
2086
|
+
if (mergedHeaders) {
|
|
2087
|
+
reqOptions.headers = mergedHeaders;
|
|
2088
|
+
}
|
|
2089
|
+
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2090
|
+
|
|
2091
|
+
const startTime = Date.now();
|
|
2092
|
+
|
|
2093
|
+
// Use httpcloak_post_raw with binary buffer (no string conversion!)
|
|
2094
|
+
const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, body, body.length, optionsJson);
|
|
2095
|
+
if (responseHandle === 0 || responseHandle === 0n || responseHandle < 0) {
|
|
2096
|
+
throw new HTTPCloakError("Failed to make POST request");
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// Get body length first (1 FFI call)
|
|
2100
|
+
const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
|
|
2101
|
+
if (bodyLen < 0) {
|
|
2102
|
+
this._lib.httpcloak_response_free(responseHandle);
|
|
2103
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// Acquire pooled buffer for response
|
|
2107
|
+
const pooledBuffer = _bufferPool.acquire(bodyLen);
|
|
2108
|
+
|
|
2109
|
+
// Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
|
|
2110
|
+
const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
|
|
2111
|
+
if (!metadataStr) {
|
|
2112
|
+
_bufferPool.release(pooledBuffer);
|
|
2113
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const metadata = JSON.parse(metadataStr);
|
|
2117
|
+
if (metadata.error) {
|
|
2118
|
+
_bufferPool.release(pooledBuffer);
|
|
2119
|
+
throw new HTTPCloakError(metadata.error);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// Create a view of just the used portion
|
|
2123
|
+
const responseBuffer = pooledBuffer.subarray(0, bodyLen);
|
|
2124
|
+
|
|
2125
|
+
const elapsed = Date.now() - startTime;
|
|
2126
|
+
return new FastResponse(metadata, responseBuffer, elapsed, pooledBuffer);
|
|
2127
|
+
}
|
|
1208
2128
|
}
|
|
1209
2129
|
|
|
1210
2130
|
// =============================================================================
|
|
@@ -1384,6 +2304,8 @@ module.exports = {
|
|
|
1384
2304
|
// Classes
|
|
1385
2305
|
Session,
|
|
1386
2306
|
Response,
|
|
2307
|
+
FastResponse,
|
|
2308
|
+
StreamResponse,
|
|
1387
2309
|
Cookie,
|
|
1388
2310
|
RedirectInfo,
|
|
1389
2311
|
HTTPCloakError,
|