httpcloak 1.6.6 → 1.6.7
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/lib/index.d.ts +255 -28
- package/lib/index.js +466 -45
- package/lib/index.mjs +7 -0
- 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-x64/package.json +1 -1
- package/package.json +6 -6
package/lib/index.d.ts
CHANGED
|
@@ -213,9 +213,9 @@ export interface SessionOptions {
|
|
|
213
213
|
allowRedirects?: boolean;
|
|
214
214
|
/** Maximum number of redirects to follow (default: 10) */
|
|
215
215
|
maxRedirects?: number;
|
|
216
|
-
/** Number of retries on failure (default:
|
|
216
|
+
/** Number of retries on failure (default: 0, opt in by setting a positive integer) */
|
|
217
217
|
retry?: number;
|
|
218
|
-
/** Status codes to retry on (default: [429, 500, 502, 503, 504]) */
|
|
218
|
+
/** Status codes to retry on (default: empty; set explicitly to opt in, e.g. [429, 500, 502, 503, 504]) */
|
|
219
219
|
retryOnStatus?: number[];
|
|
220
220
|
/** Minimum wait time between retries in milliseconds (default: 500) */
|
|
221
221
|
retryWaitMin?: number;
|
|
@@ -243,12 +243,28 @@ export interface SessionOptions {
|
|
|
243
243
|
switchProtocol?: string;
|
|
244
244
|
/** Disable internal cookie jar entirely — caller manages cookies via per-request headers (default: false) */
|
|
245
245
|
withoutCookieJar?: boolean;
|
|
246
|
+
/** Disable ETag / If-Modified-Since handling for the lifetime of the session (default: false) */
|
|
247
|
+
withoutConditionalCache?: boolean;
|
|
248
|
+
/** Skip the ECH (Encrypted Client Hello) HTTPS RR lookup. Saves ~15-20ms on first connect (default: false) */
|
|
249
|
+
disableEch?: boolean;
|
|
250
|
+
/** Disable HTTP/3 racing while keeping H1/H2 auto-negotiation. Reachable indirectly via httpVersion: "h2" but the explicit flag is cleaner (default: false) */
|
|
251
|
+
disableHttp3?: boolean;
|
|
246
252
|
/** Custom JA3 fingerprint string (e.g., "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0") */
|
|
247
253
|
ja3?: string;
|
|
248
254
|
/** Custom Akamai HTTP/2 fingerprint string (e.g., "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p") */
|
|
249
255
|
akamai?: string;
|
|
250
256
|
/** Extra fingerprint options: { tls_alpn, tls_signature_algorithms, tls_cert_compression, tls_permute_extensions } */
|
|
251
257
|
extraFp?: Record<string, any>;
|
|
258
|
+
/** TCP IP Time-To-Live in the SYN packet. 128 = Windows, 64 = Linux/macOS/iOS/Android. */
|
|
259
|
+
tcpTtl?: number;
|
|
260
|
+
/** TCP Maximum Segment Size option. 1460 for standard Ethernet. */
|
|
261
|
+
tcpMss?: number;
|
|
262
|
+
/** TCP Window Size in the SYN packet. 64240 = Windows 10/11, 65535 = Linux/macOS. */
|
|
263
|
+
tcpWindowSize?: number;
|
|
264
|
+
/** TCP Window Scale option exponent. 8 = Windows, 7 = Linux/Android, 6 = macOS/iOS. */
|
|
265
|
+
tcpWindowScale?: number;
|
|
266
|
+
/** IP Don't-Fragment flag. true on every modern client. */
|
|
267
|
+
tcpDf?: boolean;
|
|
252
268
|
}
|
|
253
269
|
|
|
254
270
|
export interface RequestOptions {
|
|
@@ -283,6 +299,64 @@ export interface RequestOptions {
|
|
|
283
299
|
* Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
|
|
284
300
|
*/
|
|
285
301
|
fetchMode?: "cors" | "no-cors" | "navigate" | "websocket";
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Per-request override for redirect following. true forces redirects on this
|
|
305
|
+
* call, false surfaces the 3xx back to the caller; null / undefined defers
|
|
306
|
+
* to the session-level setting (which itself defaults to follow).
|
|
307
|
+
*/
|
|
308
|
+
allowRedirects?: boolean | null;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Per-request opt-out of the session's ETag / If-Modified-Since handling.
|
|
312
|
+
* When true, no cache validators are injected on the way out and the
|
|
313
|
+
* response's ETag / Last-Modified are not stored. Useful for a one-off
|
|
314
|
+
* fresh fetch without touching the session-wide setting.
|
|
315
|
+
*/
|
|
316
|
+
disableConditionalCache?: boolean;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* AbortSignal for cancelling an in-flight request. Honored by the async
|
|
320
|
+
* methods (get, post, put, patch, delete, head, options, request). When
|
|
321
|
+
* the signal aborts, the underlying Go-side request is cancelled (DNS /
|
|
322
|
+
* TCP / TLS / HTTP work is torn down) and the returned promise rejects
|
|
323
|
+
* with the signal's reason (or a generic AbortError if none was set).
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* const controller = new AbortController();
|
|
327
|
+
* setTimeout(() => controller.abort(new Error("too slow")), 5000);
|
|
328
|
+
* await session.get("https://slow.example.com", { signal: controller.signal });
|
|
329
|
+
*/
|
|
330
|
+
signal?: AbortSignal;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Snapshot returned by `Session.stats()`. Mirrors the wire shape emitted by
|
|
335
|
+
* the Go-side `session.Stats()` after snake_case marshalling.
|
|
336
|
+
*/
|
|
337
|
+
export interface SessionStats {
|
|
338
|
+
/** Stable session ID assigned at construction. */
|
|
339
|
+
id: string;
|
|
340
|
+
/** Preset name the session was created with. */
|
|
341
|
+
preset: string;
|
|
342
|
+
/** Construction time, Unix nanoseconds. */
|
|
343
|
+
created_at: number;
|
|
344
|
+
/** Last-request time, Unix nanoseconds. */
|
|
345
|
+
last_used: number;
|
|
346
|
+
/** Total requests serviced by this session. */
|
|
347
|
+
request_count: number;
|
|
348
|
+
/** Whether the session is still usable (false after close). */
|
|
349
|
+
active: boolean;
|
|
350
|
+
/** Live cookie count in the jar. */
|
|
351
|
+
cookie_count: number;
|
|
352
|
+
/** Conditional-cache entry count (one per cached URL). */
|
|
353
|
+
cache_entry_count: number;
|
|
354
|
+
/** Age in nanoseconds (now - created_at). */
|
|
355
|
+
age_ns: number;
|
|
356
|
+
/** Idle time in nanoseconds (now - last_used). */
|
|
357
|
+
idle_time_ns: number;
|
|
358
|
+
/** Transport-level stats (per-protocol). Shape varies; treat as opaque. */
|
|
359
|
+
transport_stats?: Record<string, any>;
|
|
286
360
|
}
|
|
287
361
|
|
|
288
362
|
export class Session {
|
|
@@ -313,8 +387,11 @@ export class Session {
|
|
|
313
387
|
/** Refresh the session by closing all connections while keeping TLS session tickets.
|
|
314
388
|
* This simulates a browser page refresh - connections are severed but 0-RTT
|
|
315
389
|
* early data can be used on reconnection due to preserved session tickets.
|
|
390
|
+
*
|
|
391
|
+
* @param switchProtocol - Optional protocol to switch to ("h1", "h2", "h3").
|
|
392
|
+
* Overrides any switchProtocol set at construction time. Persists for future refresh() calls.
|
|
316
393
|
*/
|
|
317
|
-
refresh(): void;
|
|
394
|
+
refresh(switchProtocol?: "h1" | "h2" | "h3"): void;
|
|
318
395
|
|
|
319
396
|
// Synchronous methods
|
|
320
397
|
/** Perform a synchronous GET request */
|
|
@@ -359,17 +436,11 @@ export class Session {
|
|
|
359
436
|
/** Get a specific cookie by name with full metadata */
|
|
360
437
|
getCookieDetailed(name: string): Cookie | null;
|
|
361
438
|
|
|
362
|
-
/**
|
|
363
|
-
|
|
364
|
-
* @deprecated Will return Cookie[] with full metadata in a future release. Use getCookiesDetailed() for the new format now.
|
|
365
|
-
*/
|
|
366
|
-
getCookies(): Record<string, string>;
|
|
439
|
+
/** Get all cookies with full metadata. Alias of getCookiesDetailed(); the older flat dict shape was removed in v1.6.5. */
|
|
440
|
+
getCookies(): Cookie[];
|
|
367
441
|
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
* @deprecated Will return Cookie|null in a future release. Use getCookieDetailed() for the new format now.
|
|
371
|
-
*/
|
|
372
|
-
getCookie(name: string): string | null;
|
|
442
|
+
/** Get a specific cookie by name. Alias of getCookieDetailed(); the older value-only shape was removed in v1.6.5. */
|
|
443
|
+
getCookie(name: string): Cookie | null;
|
|
373
444
|
|
|
374
445
|
/** Set a cookie in the session */
|
|
375
446
|
setCookie(
|
|
@@ -392,11 +463,72 @@ export class Session {
|
|
|
392
463
|
/** Clear all cookies from the session */
|
|
393
464
|
clearCookies(): void;
|
|
394
465
|
|
|
466
|
+
/** Cookies in the session jar with full metadata. Same shape as getCookies(). */
|
|
467
|
+
readonly cookies: Cookie[];
|
|
468
|
+
|
|
469
|
+
// Conditional cache and redirect runtime control
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Drop the session's per-URL conditional-cache map (ETag / Last-Modified).
|
|
473
|
+
* The next request to each URL goes out without If-None-Match /
|
|
474
|
+
* If-Modified-Since headers. Cookies and TLS tickets are not touched.
|
|
475
|
+
*/
|
|
476
|
+
clearCache(): void;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Snapshot of session counters, timestamps and transport-level metrics.
|
|
480
|
+
* Mirrors the keys Go's session.Stats() returns, snake_case on the wire.
|
|
481
|
+
*/
|
|
482
|
+
stats(): SessionStats;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Return the time since the session last serviced a request, in seconds.
|
|
486
|
+
* Returns -1 if the session handle is invalid.
|
|
487
|
+
*/
|
|
488
|
+
idleTime(): number;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Return true if the session is still usable (close() has not been called
|
|
492
|
+
* and the handle is valid).
|
|
493
|
+
*/
|
|
494
|
+
isActive(): boolean;
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Reset the idle timer to now without issuing a request. Useful in
|
|
498
|
+
* long-running pools where an external heartbeat shouldn't let a session
|
|
499
|
+
* look idle to a reaper.
|
|
500
|
+
*/
|
|
501
|
+
touch(): void;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Toggle the session's ETag / If-Modified-Since handling at runtime.
|
|
505
|
+
* When disabled, the session stops injecting cache validators on outgoing
|
|
506
|
+
* requests and stops storing them from responses; the existing cache map
|
|
507
|
+
* is preserved (re-enabling resumes using it). Pair with clearCache() to
|
|
508
|
+
* also wipe previously-stored validators.
|
|
509
|
+
*/
|
|
510
|
+
setConditionalCache(enabled: boolean): void;
|
|
511
|
+
|
|
512
|
+
/** Read the session's current conditional-cache state. */
|
|
513
|
+
getConditionalCache(): boolean;
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Toggle the session's redirect-following policy at runtime. The change
|
|
517
|
+
* takes effect on the next request and persists until set again.
|
|
518
|
+
*/
|
|
519
|
+
setFollowRedirects(enabled: boolean): void;
|
|
520
|
+
|
|
521
|
+
/** Read the session's current redirect-following policy. */
|
|
522
|
+
getFollowRedirects(): boolean;
|
|
523
|
+
|
|
395
524
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
525
|
+
* Update the session's redirect cap at runtime. Values of zero or below
|
|
526
|
+
* are ignored, leaving the prior cap (or the default of 10) in place.
|
|
398
527
|
*/
|
|
399
|
-
|
|
528
|
+
setMaxRedirects(max: number): void;
|
|
529
|
+
|
|
530
|
+
/** Read the session's current redirect cap. */
|
|
531
|
+
getMaxRedirects(): number;
|
|
400
532
|
|
|
401
533
|
// Proxy management
|
|
402
534
|
|
|
@@ -657,6 +789,37 @@ export class Session {
|
|
|
657
789
|
* @returns FastResponse with Buffer body
|
|
658
790
|
*/
|
|
659
791
|
patchFast(url: string, options?: RequestOptions): FastResponse;
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Stream an arbitrary-sized body to the wire without buffering it in memory.
|
|
795
|
+
* `chunks` is any iterable / async-iterable yielding Buffer / Uint8Array /
|
|
796
|
+
* string chunks; the Go side opens an io.Pipe for the body and each chunk
|
|
797
|
+
* flows straight through with no base64 wrap and no JSON envelope.
|
|
798
|
+
*
|
|
799
|
+
* @param method Typically "POST", "PUT" or "PATCH".
|
|
800
|
+
* @param url
|
|
801
|
+
* @param chunks Iterable or async-iterable of body chunks.
|
|
802
|
+
* @param options.headers Headers (Content-Type defaults to application/octet-stream).
|
|
803
|
+
* @param options.contentType Optional explicit Content-Type override.
|
|
804
|
+
* @param options.timeout Per-request timeout in milliseconds.
|
|
805
|
+
* @returns Resolves to a regular `Response` once the upload completes.
|
|
806
|
+
*/
|
|
807
|
+
uploadStream(
|
|
808
|
+
method: string,
|
|
809
|
+
url: string,
|
|
810
|
+
chunks: AsyncIterable<Buffer | Uint8Array | string> | Iterable<Buffer | Uint8Array | string>,
|
|
811
|
+
options?: { headers?: Record<string, string>; contentType?: string; timeout?: number }
|
|
812
|
+
): Promise<Response>;
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Convenience wrapper: streaming POST. Same semantics as
|
|
816
|
+
* `uploadStream("POST", url, chunks, options)`.
|
|
817
|
+
*/
|
|
818
|
+
postUpload(
|
|
819
|
+
url: string,
|
|
820
|
+
chunks: AsyncIterable<Buffer | Uint8Array | string> | Iterable<Buffer | Uint8Array | string>,
|
|
821
|
+
options?: { headers?: Record<string, string>; contentType?: string; timeout?: number }
|
|
822
|
+
): Promise<Response>;
|
|
660
823
|
}
|
|
661
824
|
|
|
662
825
|
export interface LocalProxyOptions {
|
|
@@ -677,16 +840,20 @@ export interface LocalProxyOptions {
|
|
|
677
840
|
}
|
|
678
841
|
|
|
679
842
|
export interface LocalProxyStats {
|
|
680
|
-
/**
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
|
|
843
|
+
/** Whether the proxy is currently accepting connections */
|
|
844
|
+
running: boolean;
|
|
845
|
+
/** TCP port the proxy is bound to */
|
|
846
|
+
port: number;
|
|
847
|
+
/** Live count of in-flight connections */
|
|
848
|
+
active_conns: number;
|
|
849
|
+
/** Lifetime request counter since the proxy started */
|
|
850
|
+
total_requests: number;
|
|
851
|
+
/** Preset name the proxy was started with */
|
|
852
|
+
preset: string;
|
|
853
|
+
/** Max concurrent connections configured at start time */
|
|
854
|
+
max_connections: number;
|
|
855
|
+
/** Number of sessions currently registered via registerSession() */
|
|
856
|
+
registered_sessions: number;
|
|
690
857
|
}
|
|
691
858
|
|
|
692
859
|
/**
|
|
@@ -790,6 +957,22 @@ export class LocalProxy {
|
|
|
790
957
|
*/
|
|
791
958
|
unregisterSession(sessionId: string): boolean;
|
|
792
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Return the IDs of every session currently registered on this proxy.
|
|
962
|
+
* These are the same IDs the X-HTTPCloak-Session header accepts for
|
|
963
|
+
* per-request session routing.
|
|
964
|
+
*
|
|
965
|
+
* @returns List of registered session IDs (empty array if none).
|
|
966
|
+
*/
|
|
967
|
+
listSessions(): string[];
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Return true if a session with the given ID is currently registered.
|
|
971
|
+
* Cheaper than `listSessions().includes(id)` when callers only need an
|
|
972
|
+
* existence check (no JSON marshal across the FFI boundary).
|
|
973
|
+
*/
|
|
974
|
+
hasSession(sessionId: string): boolean;
|
|
975
|
+
|
|
793
976
|
/**
|
|
794
977
|
* Stop and close the proxy.
|
|
795
978
|
* After closing, the LocalProxy instance cannot be reused.
|
|
@@ -800,8 +983,30 @@ export class LocalProxy {
|
|
|
800
983
|
/** Get the httpcloak library version */
|
|
801
984
|
export function version(): string;
|
|
802
985
|
|
|
803
|
-
/**
|
|
804
|
-
|
|
986
|
+
/**
|
|
987
|
+
* Get available browser presets keyed by preset name.
|
|
988
|
+
*
|
|
989
|
+
* Each entry carries the protocols the preset supports (some H1/H2 only, some H1/H2/H3).
|
|
990
|
+
* The shape is `{ [presetName: string]: { protocols: string[] } }`.
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* const presets = availablePresets();
|
|
994
|
+
* Object.entries(presets).filter(([, info]) => info.protocols.includes("h3"));
|
|
995
|
+
*/
|
|
996
|
+
export function availablePresets(): Record<string, { protocols: string[] }>;
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Return a fully-resolved JSON dump of a preset's TLS / H2 / H3 / header configuration.
|
|
1000
|
+
*
|
|
1001
|
+
* Useful for inspecting what a preset name actually does at the wire level, or for
|
|
1002
|
+
* dumping a built-in preset, mutating it, and loading it back with `loadPresetFromJSON`
|
|
1003
|
+
* under a new name.
|
|
1004
|
+
*
|
|
1005
|
+
* @param name - Preset name (e.g. "chrome-148-windows", "firefox-148", "chrome-latest")
|
|
1006
|
+
* @returns JSON string. Parse with `JSON.parse` to get the structured form.
|
|
1007
|
+
* @throws {HTTPCloakError} If the preset name is not registered.
|
|
1008
|
+
*/
|
|
1009
|
+
export function describePreset(name: string): string;
|
|
805
1010
|
|
|
806
1011
|
/**
|
|
807
1012
|
* Configure the DNS servers used for ECH (Encrypted Client Hello) config queries.
|
|
@@ -859,6 +1064,28 @@ export function request(method: string, url: string, options?: RequestOptions):
|
|
|
859
1064
|
|
|
860
1065
|
/** Available browser presets */
|
|
861
1066
|
export const Preset: {
|
|
1067
|
+
CHROME_LATEST: string;
|
|
1068
|
+
CHROME_LATEST_WINDOWS: string;
|
|
1069
|
+
CHROME_LATEST_LINUX: string;
|
|
1070
|
+
CHROME_LATEST_MACOS: string;
|
|
1071
|
+
CHROME_LATEST_IOS: string;
|
|
1072
|
+
CHROME_LATEST_ANDROID: string;
|
|
1073
|
+
CHROME_149: string;
|
|
1074
|
+
CHROME_149_WINDOWS: string;
|
|
1075
|
+
CHROME_149_LINUX: string;
|
|
1076
|
+
CHROME_149_MACOS: string;
|
|
1077
|
+
CHROME_148: string;
|
|
1078
|
+
CHROME_148_WINDOWS: string;
|
|
1079
|
+
CHROME_148_LINUX: string;
|
|
1080
|
+
CHROME_148_MACOS: string;
|
|
1081
|
+
CHROME_148_IOS: string;
|
|
1082
|
+
CHROME_148_ANDROID: string;
|
|
1083
|
+
CHROME_147: string;
|
|
1084
|
+
CHROME_147_WINDOWS: string;
|
|
1085
|
+
CHROME_147_LINUX: string;
|
|
1086
|
+
CHROME_147_MACOS: string;
|
|
1087
|
+
CHROME_147_IOS: string;
|
|
1088
|
+
CHROME_147_ANDROID: string;
|
|
862
1089
|
CHROME_146: string;
|
|
863
1090
|
CHROME_146_WINDOWS: string;
|
|
864
1091
|
CHROME_146_LINUX: string;
|
package/lib/index.js
CHANGED
|
@@ -21,91 +21,143 @@ class HTTPCloakError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Available browser presets for TLS fingerprinting.
|
|
24
|
+
* Available browser presets for TLS fingerprinting. Mirrors the runtime
|
|
25
|
+
* registry shipped by the linked libhttpcloak. Use Preset.CHROME_LATEST to
|
|
26
|
+
* auto-track newest stable Chrome, or pin to a specific major version for
|
|
27
|
+
* reproducibility.
|
|
25
28
|
*
|
|
26
|
-
* Use these constants instead of typing preset strings manually:
|
|
27
29
|
* const httpcloak = require("httpcloak");
|
|
28
|
-
* httpcloak.configure({ preset: httpcloak.Preset.
|
|
30
|
+
* httpcloak.configure({ preset: httpcloak.Preset.CHROME_LATEST });
|
|
29
31
|
*
|
|
30
|
-
*
|
|
31
|
-
* const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_133 });
|
|
32
|
+
* const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_LATEST });
|
|
32
33
|
*/
|
|
33
34
|
const Preset = {
|
|
34
|
-
// Chrome
|
|
35
|
+
// Chrome latest (auto-resolves to newest shipped Chrome)
|
|
36
|
+
CHROME_LATEST: "chrome-latest",
|
|
37
|
+
CHROME_LATEST_WINDOWS: "chrome-latest-windows",
|
|
38
|
+
CHROME_LATEST_LINUX: "chrome-latest-linux",
|
|
39
|
+
CHROME_LATEST_MACOS: "chrome-latest-macos",
|
|
40
|
+
CHROME_LATEST_IOS: "chrome-latest-ios",
|
|
41
|
+
CHROME_LATEST_ANDROID: "chrome-latest-android",
|
|
42
|
+
|
|
43
|
+
// Chrome 149 (desktop; wire fingerprint identical to 148)
|
|
44
|
+
CHROME_149: "chrome-149",
|
|
45
|
+
CHROME_149_WINDOWS: "chrome-149-windows",
|
|
46
|
+
CHROME_149_LINUX: "chrome-149-linux",
|
|
47
|
+
CHROME_149_MACOS: "chrome-149-macos",
|
|
48
|
+
|
|
49
|
+
// Chrome 148
|
|
50
|
+
CHROME_148: "chrome-148",
|
|
51
|
+
CHROME_148_WINDOWS: "chrome-148-windows",
|
|
52
|
+
CHROME_148_LINUX: "chrome-148-linux",
|
|
53
|
+
CHROME_148_MACOS: "chrome-148-macos",
|
|
54
|
+
CHROME_148_IOS: "chrome-148-ios",
|
|
55
|
+
CHROME_148_ANDROID: "chrome-148-android",
|
|
56
|
+
|
|
57
|
+
// Chrome 147
|
|
58
|
+
CHROME_147: "chrome-147",
|
|
59
|
+
CHROME_147_WINDOWS: "chrome-147-windows",
|
|
60
|
+
CHROME_147_LINUX: "chrome-147-linux",
|
|
61
|
+
CHROME_147_MACOS: "chrome-147-macos",
|
|
62
|
+
CHROME_147_IOS: "chrome-147-ios",
|
|
63
|
+
CHROME_147_ANDROID: "chrome-147-android",
|
|
64
|
+
|
|
65
|
+
// Chrome 146
|
|
35
66
|
CHROME_146: "chrome-146",
|
|
36
67
|
CHROME_146_WINDOWS: "chrome-146-windows",
|
|
37
68
|
CHROME_146_LINUX: "chrome-146-linux",
|
|
38
69
|
CHROME_146_MACOS: "chrome-146-macos",
|
|
70
|
+
CHROME_146_IOS: "chrome-146-ios",
|
|
71
|
+
CHROME_146_ANDROID: "chrome-146-android",
|
|
39
72
|
|
|
40
73
|
// Chrome 145
|
|
41
74
|
CHROME_145: "chrome-145",
|
|
42
75
|
CHROME_145_WINDOWS: "chrome-145-windows",
|
|
43
76
|
CHROME_145_LINUX: "chrome-145-linux",
|
|
44
77
|
CHROME_145_MACOS: "chrome-145-macos",
|
|
78
|
+
CHROME_145_IOS: "chrome-145-ios",
|
|
79
|
+
CHROME_145_ANDROID: "chrome-145-android",
|
|
45
80
|
|
|
46
81
|
// Chrome 144
|
|
47
82
|
CHROME_144: "chrome-144",
|
|
48
83
|
CHROME_144_WINDOWS: "chrome-144-windows",
|
|
49
84
|
CHROME_144_LINUX: "chrome-144-linux",
|
|
50
85
|
CHROME_144_MACOS: "chrome-144-macos",
|
|
86
|
+
CHROME_144_IOS: "chrome-144-ios",
|
|
87
|
+
CHROME_144_ANDROID: "chrome-144-android",
|
|
51
88
|
|
|
52
89
|
// Chrome 143
|
|
53
90
|
CHROME_143: "chrome-143",
|
|
54
91
|
CHROME_143_WINDOWS: "chrome-143-windows",
|
|
55
92
|
CHROME_143_LINUX: "chrome-143-linux",
|
|
56
93
|
CHROME_143_MACOS: "chrome-143-macos",
|
|
94
|
+
CHROME_143_IOS: "chrome-143-ios",
|
|
95
|
+
CHROME_143_ANDROID: "chrome-143-android",
|
|
57
96
|
|
|
58
|
-
// Chrome
|
|
97
|
+
// Older Chrome (H1/H2 only, no H3)
|
|
59
98
|
CHROME_141: "chrome-141",
|
|
60
|
-
|
|
61
|
-
// Chrome 133
|
|
62
99
|
CHROME_133: "chrome-133",
|
|
63
100
|
|
|
64
|
-
// Mobile Chrome
|
|
65
|
-
CHROME_143_IOS: "chrome-143-ios",
|
|
66
|
-
CHROME_144_IOS: "chrome-144-ios",
|
|
67
|
-
CHROME_145_IOS: "chrome-145-ios",
|
|
68
|
-
CHROME_146_IOS: "chrome-146-ios",
|
|
69
|
-
CHROME_143_ANDROID: "chrome-143-android",
|
|
70
|
-
CHROME_144_ANDROID: "chrome-144-android",
|
|
71
|
-
CHROME_145_ANDROID: "chrome-145-android",
|
|
72
|
-
CHROME_146_ANDROID: "chrome-146-android",
|
|
73
|
-
|
|
74
101
|
// Firefox
|
|
102
|
+
FIREFOX_LATEST: "firefox-latest",
|
|
103
|
+
FIREFOX_148: "firefox-148",
|
|
75
104
|
FIREFOX_133: "firefox-133",
|
|
76
105
|
|
|
77
106
|
// Safari (desktop and mobile)
|
|
107
|
+
SAFARI_LATEST: "safari-latest",
|
|
78
108
|
SAFARI_18: "safari-18",
|
|
79
109
|
SAFARI_17_IOS: "safari-17-ios",
|
|
80
110
|
SAFARI_18_IOS: "safari-18-ios",
|
|
111
|
+
SAFARI_LATEST_IOS: "safari-latest-ios",
|
|
81
112
|
|
|
82
|
-
// Backwards compatibility aliases (old naming
|
|
113
|
+
// Backwards compatibility aliases (old "ios-chrome" / "android-chrome" naming)
|
|
83
114
|
IOS_CHROME_143: "chrome-143-ios",
|
|
84
115
|
IOS_CHROME_144: "chrome-144-ios",
|
|
85
116
|
IOS_CHROME_145: "chrome-145-ios",
|
|
86
117
|
IOS_CHROME_146: "chrome-146-ios",
|
|
118
|
+
IOS_CHROME_147: "chrome-147-ios",
|
|
119
|
+
IOS_CHROME_148: "chrome-148-ios",
|
|
120
|
+
IOS_CHROME_LATEST: "chrome-latest-ios",
|
|
87
121
|
ANDROID_CHROME_143: "chrome-143-android",
|
|
88
122
|
ANDROID_CHROME_144: "chrome-144-android",
|
|
89
123
|
ANDROID_CHROME_145: "chrome-145-android",
|
|
90
124
|
ANDROID_CHROME_146: "chrome-146-android",
|
|
125
|
+
ANDROID_CHROME_147: "chrome-147-android",
|
|
126
|
+
ANDROID_CHROME_148: "chrome-148-android",
|
|
127
|
+
ANDROID_CHROME_LATEST: "chrome-latest-android",
|
|
91
128
|
IOS_SAFARI_17: "safari-17-ios",
|
|
92
129
|
IOS_SAFARI_18: "safari-18-ios",
|
|
130
|
+
IOS_SAFARI_LATEST: "safari-latest-ios",
|
|
93
131
|
|
|
94
132
|
/**
|
|
95
|
-
* Get all
|
|
96
|
-
*
|
|
133
|
+
* Get all built-in preset names known to this binding version.
|
|
134
|
+
*
|
|
135
|
+
* For the authoritative live list (which may include custom presets
|
|
136
|
+
* loaded at runtime), call `availablePresets()` instead.
|
|
137
|
+
*
|
|
138
|
+
* @returns {string[]} List of preset names
|
|
97
139
|
*/
|
|
98
140
|
all() {
|
|
99
141
|
return [
|
|
142
|
+
this.CHROME_LATEST, this.CHROME_LATEST_WINDOWS, this.CHROME_LATEST_LINUX,
|
|
143
|
+
this.CHROME_LATEST_MACOS, this.CHROME_LATEST_IOS, this.CHROME_LATEST_ANDROID,
|
|
144
|
+
this.CHROME_149, this.CHROME_149_WINDOWS, this.CHROME_149_LINUX, this.CHROME_149_MACOS,
|
|
145
|
+
this.CHROME_148, this.CHROME_148_WINDOWS, this.CHROME_148_LINUX, this.CHROME_148_MACOS,
|
|
146
|
+
this.CHROME_148_IOS, this.CHROME_148_ANDROID,
|
|
147
|
+
this.CHROME_147, this.CHROME_147_WINDOWS, this.CHROME_147_LINUX, this.CHROME_147_MACOS,
|
|
148
|
+
this.CHROME_147_IOS, this.CHROME_147_ANDROID,
|
|
100
149
|
this.CHROME_146, this.CHROME_146_WINDOWS, this.CHROME_146_LINUX, this.CHROME_146_MACOS,
|
|
150
|
+
this.CHROME_146_IOS, this.CHROME_146_ANDROID,
|
|
101
151
|
this.CHROME_145, this.CHROME_145_WINDOWS, this.CHROME_145_LINUX, this.CHROME_145_MACOS,
|
|
152
|
+
this.CHROME_145_IOS, this.CHROME_145_ANDROID,
|
|
102
153
|
this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
|
|
154
|
+
this.CHROME_144_IOS, this.CHROME_144_ANDROID,
|
|
103
155
|
this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
|
|
156
|
+
this.CHROME_143_IOS, this.CHROME_143_ANDROID,
|
|
104
157
|
this.CHROME_141, this.CHROME_133,
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
108
|
-
this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
|
|
158
|
+
this.FIREFOX_LATEST, this.FIREFOX_148, this.FIREFOX_133,
|
|
159
|
+
this.SAFARI_LATEST, this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
|
|
160
|
+
this.SAFARI_LATEST_IOS,
|
|
109
161
|
];
|
|
110
162
|
},
|
|
111
163
|
};
|
|
@@ -830,6 +882,18 @@ function getLib() {
|
|
|
830
882
|
httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
|
|
831
883
|
httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
|
|
832
884
|
httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
|
|
885
|
+
httpcloak_session_clear_cache: nativeLibHandle.func("httpcloak_session_clear_cache", "void", ["int64"]),
|
|
886
|
+
httpcloak_session_stats: nativeLibHandle.func("httpcloak_session_stats", HeapStr, ["int64"]),
|
|
887
|
+
httpcloak_session_idle_time: nativeLibHandle.func("httpcloak_session_idle_time", "int64", ["int64"]),
|
|
888
|
+
httpcloak_session_is_active: nativeLibHandle.func("httpcloak_session_is_active", "int", ["int64"]),
|
|
889
|
+
httpcloak_session_touch: nativeLibHandle.func("httpcloak_session_touch", "void", ["int64"]),
|
|
890
|
+
httpcloak_session_set_conditional_cache: nativeLibHandle.func("httpcloak_session_set_conditional_cache", "void", ["int64", "int"]),
|
|
891
|
+
httpcloak_session_get_conditional_cache: nativeLibHandle.func("httpcloak_session_get_conditional_cache", "int", ["int64"]),
|
|
892
|
+
httpcloak_session_set_follow_redirects: nativeLibHandle.func("httpcloak_session_set_follow_redirects", "void", ["int64", "int"]),
|
|
893
|
+
httpcloak_session_get_follow_redirects: nativeLibHandle.func("httpcloak_session_get_follow_redirects", "int", ["int64"]),
|
|
894
|
+
httpcloak_session_set_max_redirects: nativeLibHandle.func("httpcloak_session_set_max_redirects", "void", ["int64", "int"]),
|
|
895
|
+
httpcloak_session_get_max_redirects: nativeLibHandle.func("httpcloak_session_get_max_redirects", "int", ["int64"]),
|
|
896
|
+
httpcloak_session_set_identifier: nativeLibHandle.func("httpcloak_session_set_identifier", "void", ["int64", "str"]),
|
|
833
897
|
httpcloak_free_string: httpcloakFreeString,
|
|
834
898
|
httpcloak_version: nativeLibHandle.func("httpcloak_version", HeapStr, []),
|
|
835
899
|
httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", HeapStr, []),
|
|
@@ -841,6 +905,7 @@ function getLib() {
|
|
|
841
905
|
httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
|
|
842
906
|
httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
|
|
843
907
|
httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
|
|
908
|
+
httpcloak_cancel_request: nativeLibHandle.func("httpcloak_cancel_request", "void", ["int64"]),
|
|
844
909
|
// Streaming functions
|
|
845
910
|
httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
|
|
846
911
|
httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
|
|
@@ -848,6 +913,15 @@ function getLib() {
|
|
|
848
913
|
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", HeapStr, ["int64"]),
|
|
849
914
|
httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", HeapStr, ["int64", "int64"]),
|
|
850
915
|
httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
|
|
916
|
+
// Chunked upload state machine (mirrors Python's stream_upload). Lets
|
|
917
|
+
// bindings ship arbitrarily-large bodies without buffering in memory:
|
|
918
|
+
// upload_start opens a pipe to the Go side, upload_write_raw streams
|
|
919
|
+
// chunks straight to the wire, upload_finish reads the response,
|
|
920
|
+
// upload_cancel tears down a partial upload on error.
|
|
921
|
+
httpcloak_upload_start: nativeLibHandle.func("httpcloak_upload_start", "int64", ["int64", "str", "str"]),
|
|
922
|
+
httpcloak_upload_write_raw: nativeLibHandle.func("httpcloak_upload_write_raw", "int", ["int64", "void*", "int"]),
|
|
923
|
+
httpcloak_upload_finish: nativeLibHandle.func("httpcloak_upload_finish", HeapStr, ["int64"]),
|
|
924
|
+
httpcloak_upload_cancel: nativeLibHandle.func("httpcloak_upload_cancel", "void", ["int64"]),
|
|
851
925
|
// Raw response functions for fast-path (zero-copy)
|
|
852
926
|
httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
|
|
853
927
|
httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
|
|
@@ -881,6 +955,8 @@ function getLib() {
|
|
|
881
955
|
httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", HeapStr, ["int64"]),
|
|
882
956
|
httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", HeapStr, ["int64", "str", "int64"]),
|
|
883
957
|
httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
|
|
958
|
+
httpcloak_local_proxy_list_sessions: nativeLibHandle.func("httpcloak_local_proxy_list_sessions", HeapStr, ["int64"]),
|
|
959
|
+
httpcloak_local_proxy_has_session: nativeLibHandle.func("httpcloak_local_proxy_has_session", "int", ["int64", "str"]),
|
|
884
960
|
// Session cache callbacks
|
|
885
961
|
httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
|
|
886
962
|
koffi.pointer(SessionCacheGetProto),
|
|
@@ -1010,7 +1086,7 @@ class AsyncCallbackManager {
|
|
|
1010
1086
|
* Register a new async request
|
|
1011
1087
|
* @returns {{ callbackId: number, promise: Promise<Response> }}
|
|
1012
1088
|
*/
|
|
1013
|
-
registerRequest(nativeLib) {
|
|
1089
|
+
registerRequest(nativeLib, signal) {
|
|
1014
1090
|
this._ensureCallback();
|
|
1015
1091
|
|
|
1016
1092
|
// Register callback with Go (each request gets unique ID)
|
|
@@ -1027,6 +1103,43 @@ class AsyncCallbackManager {
|
|
|
1027
1103
|
this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
|
|
1028
1104
|
this._ref(); // Keep event loop alive
|
|
1029
1105
|
|
|
1106
|
+
// AbortSignal integration. If the caller supplies a signal that is
|
|
1107
|
+
// already aborted, cancel synchronously; otherwise install a listener
|
|
1108
|
+
// that fires once. Pre-aborted signals never deliver the "abort" event,
|
|
1109
|
+
// so the explicit early-path matters.
|
|
1110
|
+
//
|
|
1111
|
+
// Order matters in onAbort:
|
|
1112
|
+
// 1) cancel_request → unblocks the Go goroutine (ctx.Done fires)
|
|
1113
|
+
// 2) unregister_callback → removes the entry from Go's asyncCallbacks
|
|
1114
|
+
// map so the goroutine's final invokeCallback() finds !exists and
|
|
1115
|
+
// returns silently. Without this, Go would still relay an error
|
|
1116
|
+
// callback through koffi after the JS side has already settled
|
|
1117
|
+
// and torn down its callback context, which crashes node with
|
|
1118
|
+
// "Error::ThrowAsJavaScriptException napi_throw" during exit.
|
|
1119
|
+
if (signal && typeof signal === "object" && "aborted" in signal) {
|
|
1120
|
+
const onAbort = () => {
|
|
1121
|
+
const cid = Number(callbackId);
|
|
1122
|
+
try {
|
|
1123
|
+
nativeLib.httpcloak_cancel_request(cid);
|
|
1124
|
+
nativeLib.httpcloak_unregister_callback(cid);
|
|
1125
|
+
} catch {
|
|
1126
|
+
// Best-effort: the request may have already settled.
|
|
1127
|
+
}
|
|
1128
|
+
const pending = this._pendingRequests.get(cid);
|
|
1129
|
+
if (pending) {
|
|
1130
|
+
this._pendingRequests.delete(cid);
|
|
1131
|
+
this._unref();
|
|
1132
|
+
const reason = signal.reason ?? new Error("AbortError");
|
|
1133
|
+
pending.reject(reason);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
if (signal.aborted) {
|
|
1137
|
+
onAbort();
|
|
1138
|
+
} else if (typeof signal.addEventListener === "function") {
|
|
1139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1030
1143
|
return { callbackId, promise };
|
|
1031
1144
|
}
|
|
1032
1145
|
}
|
|
@@ -1362,6 +1475,9 @@ class Session {
|
|
|
1362
1475
|
enableSpeculativeTls = false,
|
|
1363
1476
|
switchProtocol = null,
|
|
1364
1477
|
withoutCookieJar = false,
|
|
1478
|
+
withoutConditionalCache = false,
|
|
1479
|
+
disableEch = false,
|
|
1480
|
+
disableHttp3 = false,
|
|
1365
1481
|
ja3 = null,
|
|
1366
1482
|
akamai = null,
|
|
1367
1483
|
extraFp = null,
|
|
@@ -1439,6 +1555,15 @@ class Session {
|
|
|
1439
1555
|
if (withoutCookieJar) {
|
|
1440
1556
|
config.without_cookie_jar = true;
|
|
1441
1557
|
}
|
|
1558
|
+
if (withoutConditionalCache) {
|
|
1559
|
+
config.without_conditional_cache = true;
|
|
1560
|
+
}
|
|
1561
|
+
if (disableEch) {
|
|
1562
|
+
config.disable_ech = true;
|
|
1563
|
+
}
|
|
1564
|
+
if (disableHttp3) {
|
|
1565
|
+
config.disable_http3 = true;
|
|
1566
|
+
}
|
|
1442
1567
|
if (ja3) {
|
|
1443
1568
|
config.ja3 = ja3;
|
|
1444
1569
|
}
|
|
@@ -1602,7 +1727,7 @@ class Session {
|
|
|
1602
1727
|
* @returns {Response} Response object
|
|
1603
1728
|
*/
|
|
1604
1729
|
getSync(url, options = {}) {
|
|
1605
|
-
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1730
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false } = options;
|
|
1606
1731
|
|
|
1607
1732
|
url = addParamsToUrl(url, params);
|
|
1608
1733
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1619,6 +1744,12 @@ class Session {
|
|
|
1619
1744
|
if (fetchMode) {
|
|
1620
1745
|
reqOptions.fetch_mode = fetchMode;
|
|
1621
1746
|
}
|
|
1747
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1748
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1749
|
+
}
|
|
1750
|
+
if (disableConditionalCache) {
|
|
1751
|
+
reqOptions.disable_conditional_cache = true;
|
|
1752
|
+
}
|
|
1622
1753
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1623
1754
|
|
|
1624
1755
|
const startTime = Date.now();
|
|
@@ -1648,7 +1779,7 @@ class Session {
|
|
|
1648
1779
|
* @returns {Response} Response object
|
|
1649
1780
|
*/
|
|
1650
1781
|
postSync(url, options = {}) {
|
|
1651
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1782
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false } = options;
|
|
1652
1783
|
|
|
1653
1784
|
url = addParamsToUrl(url, params);
|
|
1654
1785
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1693,6 +1824,12 @@ class Session {
|
|
|
1693
1824
|
if (fetchMode) {
|
|
1694
1825
|
reqOptions.fetch_mode = fetchMode;
|
|
1695
1826
|
}
|
|
1827
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1828
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1829
|
+
}
|
|
1830
|
+
if (disableConditionalCache) {
|
|
1831
|
+
reqOptions.disable_conditional_cache = true;
|
|
1832
|
+
}
|
|
1696
1833
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1697
1834
|
|
|
1698
1835
|
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
@@ -1717,7 +1854,7 @@ class Session {
|
|
|
1717
1854
|
* @returns {Response} Response object
|
|
1718
1855
|
*/
|
|
1719
1856
|
requestSync(method, url, options = {}) {
|
|
1720
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
1857
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false } = options;
|
|
1721
1858
|
|
|
1722
1859
|
url = addParamsToUrl(url, params);
|
|
1723
1860
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1760,6 +1897,8 @@ class Session {
|
|
|
1760
1897
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1761
1898
|
if (timeout) requestConfig.timeout = timeout;
|
|
1762
1899
|
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1900
|
+
if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
|
|
1901
|
+
if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
|
|
1763
1902
|
|
|
1764
1903
|
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1765
1904
|
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
@@ -1790,7 +1929,7 @@ class Session {
|
|
|
1790
1929
|
* @returns {Promise<Response>} Response object
|
|
1791
1930
|
*/
|
|
1792
1931
|
get(url, options = {}) {
|
|
1793
|
-
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1932
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, signal = null } = options;
|
|
1794
1933
|
|
|
1795
1934
|
url = addParamsToUrl(url, params);
|
|
1796
1935
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1813,11 +1952,17 @@ class Session {
|
|
|
1813
1952
|
// httpcloak_request_async — that path also uses time.Second).
|
|
1814
1953
|
reqOptions.timeout = timeout;
|
|
1815
1954
|
}
|
|
1955
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1956
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1957
|
+
}
|
|
1958
|
+
if (disableConditionalCache) {
|
|
1959
|
+
reqOptions.disable_conditional_cache = true;
|
|
1960
|
+
}
|
|
1816
1961
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1817
1962
|
|
|
1818
1963
|
// Register async request with callback manager
|
|
1819
1964
|
const manager = getAsyncManager();
|
|
1820
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1965
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1821
1966
|
|
|
1822
1967
|
// Start async request
|
|
1823
1968
|
this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
|
|
@@ -1833,16 +1978,23 @@ class Session {
|
|
|
1833
1978
|
* @returns {Promise<Response>} Response object
|
|
1834
1979
|
*/
|
|
1835
1980
|
post(url, options = {}) {
|
|
1836
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1981
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, signal = null } = options;
|
|
1837
1982
|
|
|
1838
1983
|
url = addParamsToUrl(url, params);
|
|
1839
1984
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1840
1985
|
|
|
1986
|
+
// bodyEncoding tells clib whether `body` is plain text or base64-encoded
|
|
1987
|
+
// binary. Default "" = text. Anything that carries arbitrary bytes
|
|
1988
|
+
// (Buffer, multipart) flows through as base64 so NUL bytes don't truncate
|
|
1989
|
+
// the C string at the cgo boundary.
|
|
1990
|
+
let bodyEncoding = "";
|
|
1991
|
+
|
|
1841
1992
|
// Handle multipart file upload
|
|
1842
1993
|
if (files !== null) {
|
|
1843
1994
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1844
1995
|
const multipart = encodeMultipart(formData, files);
|
|
1845
|
-
body = multipart.body.toString("
|
|
1996
|
+
body = multipart.body.toString("base64");
|
|
1997
|
+
bodyEncoding = "base64";
|
|
1846
1998
|
mergedHeaders = mergedHeaders || {};
|
|
1847
1999
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1848
2000
|
}
|
|
@@ -1862,9 +2014,11 @@ class Session {
|
|
|
1862
2014
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1863
2015
|
}
|
|
1864
2016
|
}
|
|
1865
|
-
// Handle Buffer body
|
|
2017
|
+
// Handle Buffer body (binary payload). base64-encode so NUL bytes and
|
|
2018
|
+
// non-UTF-8 sequences survive the cgo C-string round-trip intact.
|
|
1866
2019
|
else if (Buffer.isBuffer(body)) {
|
|
1867
|
-
body = body.toString("
|
|
2020
|
+
body = body.toString("base64");
|
|
2021
|
+
bodyEncoding = "base64";
|
|
1868
2022
|
}
|
|
1869
2023
|
|
|
1870
2024
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1884,11 +2038,20 @@ class Session {
|
|
|
1884
2038
|
// Public API: seconds. clib post_async path enforces in seconds.
|
|
1885
2039
|
reqOptions.timeout = timeout;
|
|
1886
2040
|
}
|
|
2041
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2042
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2043
|
+
}
|
|
2044
|
+
if (disableConditionalCache) {
|
|
2045
|
+
reqOptions.disable_conditional_cache = true;
|
|
2046
|
+
}
|
|
2047
|
+
if (bodyEncoding) {
|
|
2048
|
+
reqOptions.body_encoding = bodyEncoding;
|
|
2049
|
+
}
|
|
1887
2050
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1888
2051
|
|
|
1889
2052
|
// Register async request with callback manager
|
|
1890
2053
|
const manager = getAsyncManager();
|
|
1891
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
2054
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1892
2055
|
|
|
1893
2056
|
// Start async request
|
|
1894
2057
|
this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
|
|
@@ -1905,16 +2068,22 @@ class Session {
|
|
|
1905
2068
|
* @returns {Promise<Response>} Response object
|
|
1906
2069
|
*/
|
|
1907
2070
|
request(method, url, options = {}) {
|
|
1908
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
2071
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, signal = null } = options;
|
|
1909
2072
|
|
|
1910
2073
|
url = addParamsToUrl(url, params);
|
|
1911
2074
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1912
2075
|
|
|
2076
|
+
// bodyEncoding tells clib whether `body` is text or base64 binary.
|
|
2077
|
+
// Binary payloads (Buffer / multipart) flow through as base64 so they
|
|
2078
|
+
// survive JSON serialization + the cgo C-string boundary intact.
|
|
2079
|
+
let bodyEncoding = "";
|
|
2080
|
+
|
|
1913
2081
|
// Handle multipart file upload
|
|
1914
2082
|
if (files !== null) {
|
|
1915
2083
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1916
2084
|
const multipart = encodeMultipart(formData, files);
|
|
1917
|
-
body = multipart.body.toString("
|
|
2085
|
+
body = multipart.body.toString("base64");
|
|
2086
|
+
bodyEncoding = "base64";
|
|
1918
2087
|
mergedHeaders = mergedHeaders || {};
|
|
1919
2088
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1920
2089
|
}
|
|
@@ -1934,9 +2103,11 @@ class Session {
|
|
|
1934
2103
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1935
2104
|
}
|
|
1936
2105
|
}
|
|
1937
|
-
// Handle Buffer body
|
|
2106
|
+
// Handle Buffer body (binary payload). base64-encode so non-UTF-8 bytes
|
|
2107
|
+
// and NUL bytes survive JSON.stringify + the C boundary.
|
|
1938
2108
|
else if (Buffer.isBuffer(body)) {
|
|
1939
|
-
body = body.toString("
|
|
2109
|
+
body = body.toString("base64");
|
|
2110
|
+
bodyEncoding = "base64";
|
|
1940
2111
|
}
|
|
1941
2112
|
|
|
1942
2113
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1950,12 +2121,15 @@ class Session {
|
|
|
1950
2121
|
};
|
|
1951
2122
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1952
2123
|
if (body) requestConfig.body = body;
|
|
2124
|
+
if (bodyEncoding) requestConfig.body_encoding = bodyEncoding;
|
|
1953
2125
|
if (timeout) requestConfig.timeout = timeout;
|
|
1954
2126
|
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
2127
|
+
if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
|
|
2128
|
+
if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
|
|
1955
2129
|
|
|
1956
2130
|
// Register async request with callback manager
|
|
1957
2131
|
const manager = getAsyncManager();
|
|
1958
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
2132
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1959
2133
|
|
|
1960
2134
|
// Start async request
|
|
1961
2135
|
this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
|
|
@@ -2103,6 +2277,122 @@ class Session {
|
|
|
2103
2277
|
return this.getCookies();
|
|
2104
2278
|
}
|
|
2105
2279
|
|
|
2280
|
+
// ===========================================================================
|
|
2281
|
+
// Conditional Cache and Redirect Runtime Control
|
|
2282
|
+
// ===========================================================================
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* Drop the session's per-URL conditional-cache map (ETag / Last-Modified).
|
|
2286
|
+
* The next request to each URL goes out without If-None-Match /
|
|
2287
|
+
* If-Modified-Since headers. Cookies and TLS tickets are not touched.
|
|
2288
|
+
*/
|
|
2289
|
+
clearCache() {
|
|
2290
|
+
this._lib.httpcloak_session_clear_cache(this._handle);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Return a snapshot of session counters, timestamps and transport-level
|
|
2295
|
+
* metrics. Useful for long-running scrapers that want per-session metrics
|
|
2296
|
+
* scraped into Prometheus / Datadog / etc.
|
|
2297
|
+
*
|
|
2298
|
+
* Keys: id, preset, created_at (Unix ns), last_used (Unix ns),
|
|
2299
|
+
* request_count, active, cookie_count, cache_entry_count, age_ns,
|
|
2300
|
+
* idle_time_ns, transport_stats (per-protocol object).
|
|
2301
|
+
*
|
|
2302
|
+
* @returns {Object} Stats snapshot, or empty object if the session is closed.
|
|
2303
|
+
*/
|
|
2304
|
+
stats() {
|
|
2305
|
+
const ptr = this._lib.httpcloak_session_stats(this._handle);
|
|
2306
|
+
const raw = resultToString(ptr);
|
|
2307
|
+
if (!raw) return {};
|
|
2308
|
+
try { return JSON.parse(raw); } catch { return {}; }
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Return the time since the session last serviced a request, in seconds.
|
|
2313
|
+
* Returns -1 if the session handle is invalid.
|
|
2314
|
+
*
|
|
2315
|
+
* @returns {number} Idle time in seconds (may be fractional).
|
|
2316
|
+
*/
|
|
2317
|
+
idleTime() {
|
|
2318
|
+
const ns = Number(this._lib.httpcloak_session_idle_time(this._handle));
|
|
2319
|
+
if (ns < 0) return -1;
|
|
2320
|
+
return ns / 1_000_000_000;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* Return true if the session is still usable (close() has not been called
|
|
2325
|
+
* and the handle is valid).
|
|
2326
|
+
*
|
|
2327
|
+
* @returns {boolean}
|
|
2328
|
+
*/
|
|
2329
|
+
isActive() {
|
|
2330
|
+
return this._lib.httpcloak_session_is_active(this._handle) === 1;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* Reset the idle timer to now without issuing a request. Useful in
|
|
2335
|
+
* long-running pools where an external heartbeat shouldn't let a session
|
|
2336
|
+
* look idle to a reaper.
|
|
2337
|
+
*/
|
|
2338
|
+
touch() {
|
|
2339
|
+
this._lib.httpcloak_session_touch(this._handle);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/**
|
|
2343
|
+
* Toggle the session's ETag / If-Modified-Since handling at runtime.
|
|
2344
|
+
* When disabled, the session stops injecting cache validators on outgoing
|
|
2345
|
+
* requests and stops storing them from responses; the existing cache map
|
|
2346
|
+
* is preserved (re-enabling resumes using it). Pair with clearCache() to
|
|
2347
|
+
* also wipe previously-stored validators.
|
|
2348
|
+
* @param {boolean} enabled
|
|
2349
|
+
*/
|
|
2350
|
+
setConditionalCache(enabled) {
|
|
2351
|
+
this._lib.httpcloak_session_set_conditional_cache(this._handle, enabled ? 1 : 0);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Return the session's current conditional-cache state.
|
|
2356
|
+
* @returns {boolean}
|
|
2357
|
+
*/
|
|
2358
|
+
getConditionalCache() {
|
|
2359
|
+
return this._lib.httpcloak_session_get_conditional_cache(this._handle) !== 0;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/**
|
|
2363
|
+
* Toggle the session's redirect-following policy at runtime. The change
|
|
2364
|
+
* takes effect on the next request and persists until set again.
|
|
2365
|
+
* @param {boolean} enabled
|
|
2366
|
+
*/
|
|
2367
|
+
setFollowRedirects(enabled) {
|
|
2368
|
+
this._lib.httpcloak_session_set_follow_redirects(this._handle, enabled ? 1 : 0);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/**
|
|
2372
|
+
* Return the session's current redirect-following policy.
|
|
2373
|
+
* @returns {boolean}
|
|
2374
|
+
*/
|
|
2375
|
+
getFollowRedirects() {
|
|
2376
|
+
return this._lib.httpcloak_session_get_follow_redirects(this._handle) !== 0;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/**
|
|
2380
|
+
* Update the session's redirect cap at runtime. Values of zero or below
|
|
2381
|
+
* are ignored, leaving the prior cap (or the default of 10) in place.
|
|
2382
|
+
* @param {number} max
|
|
2383
|
+
*/
|
|
2384
|
+
setMaxRedirects(max) {
|
|
2385
|
+
this._lib.httpcloak_session_set_max_redirects(this._handle, max);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
/**
|
|
2389
|
+
* Return the session's current redirect cap.
|
|
2390
|
+
* @returns {number}
|
|
2391
|
+
*/
|
|
2392
|
+
getMaxRedirects() {
|
|
2393
|
+
return this._lib.httpcloak_session_get_max_redirects(this._handle);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2106
2396
|
// ===========================================================================
|
|
2107
2397
|
// Proxy Management
|
|
2108
2398
|
// ===========================================================================
|
|
@@ -2399,7 +2689,7 @@ class Session {
|
|
|
2399
2689
|
* stream.close();
|
|
2400
2690
|
*/
|
|
2401
2691
|
getStream(url, options = {}) {
|
|
2402
|
-
const { params, headers, cookies, timeout } = options;
|
|
2692
|
+
const { params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
|
|
2403
2693
|
|
|
2404
2694
|
// Add params to URL
|
|
2405
2695
|
if (params) {
|
|
@@ -2426,6 +2716,12 @@ class Session {
|
|
|
2426
2716
|
if (timeout) {
|
|
2427
2717
|
reqOptions.timeout = timeout;
|
|
2428
2718
|
}
|
|
2719
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2720
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2721
|
+
}
|
|
2722
|
+
if (disableConditionalCache) {
|
|
2723
|
+
reqOptions.disable_conditional_cache = true;
|
|
2724
|
+
}
|
|
2429
2725
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2430
2726
|
|
|
2431
2727
|
// Start stream
|
|
@@ -2465,7 +2761,7 @@ class Session {
|
|
|
2465
2761
|
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
2466
2762
|
*/
|
|
2467
2763
|
postStream(url, options = {}) {
|
|
2468
|
-
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
|
|
2764
|
+
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
|
|
2469
2765
|
|
|
2470
2766
|
// Add params to URL
|
|
2471
2767
|
if (params) {
|
|
@@ -2505,6 +2801,12 @@ class Session {
|
|
|
2505
2801
|
if (timeout) {
|
|
2506
2802
|
reqOptions.timeout = timeout;
|
|
2507
2803
|
}
|
|
2804
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2805
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2806
|
+
}
|
|
2807
|
+
if (disableConditionalCache) {
|
|
2808
|
+
reqOptions.disable_conditional_cache = true;
|
|
2809
|
+
}
|
|
2508
2810
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2509
2811
|
|
|
2510
2812
|
// Start stream
|
|
@@ -2543,7 +2845,7 @@ class Session {
|
|
|
2543
2845
|
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
2544
2846
|
*/
|
|
2545
2847
|
requestStream(method, url, options = {}) {
|
|
2546
|
-
const { body, params, headers, cookies, timeout } = options;
|
|
2848
|
+
const { body, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
|
|
2547
2849
|
|
|
2548
2850
|
// Add params to URL
|
|
2549
2851
|
if (params) {
|
|
@@ -2576,6 +2878,12 @@ class Session {
|
|
|
2576
2878
|
if (timeout) {
|
|
2577
2879
|
requestConfig.timeout = timeout;
|
|
2578
2880
|
}
|
|
2881
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2882
|
+
requestConfig.follow_redirects = !!allowRedirects;
|
|
2883
|
+
}
|
|
2884
|
+
if (disableConditionalCache) {
|
|
2885
|
+
requestConfig.disable_conditional_cache = true;
|
|
2886
|
+
}
|
|
2579
2887
|
|
|
2580
2888
|
// Start stream
|
|
2581
2889
|
const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
|
|
@@ -2894,6 +3202,84 @@ class Session {
|
|
|
2894
3202
|
patchFast(url, options = {}) {
|
|
2895
3203
|
return this.requestFast("PATCH", url, options);
|
|
2896
3204
|
}
|
|
3205
|
+
|
|
3206
|
+
/**
|
|
3207
|
+
* Stream an arbitrary-sized body to the wire without buffering in memory.
|
|
3208
|
+
*
|
|
3209
|
+
* `chunks` is anything iterable that yields Buffer-shaped objects: an
|
|
3210
|
+
* AsyncIterable<Buffer> (e.g. an `fs.createReadStream(path)`), an
|
|
3211
|
+
* Iterable<Buffer> (e.g. an array), or a sync generator that yields
|
|
3212
|
+
* Buffers. String chunks are auto-encoded as UTF-8.
|
|
3213
|
+
*
|
|
3214
|
+
* The Go side opens an `io.Pipe()` for the body when uploadStart returns;
|
|
3215
|
+
* each chunk is written straight through with no base64 / no JSON wrapping.
|
|
3216
|
+
* On error the partial upload is cancelled and the underlying connection
|
|
3217
|
+
* closed; on success uploadFinish reads the full response and parses it
|
|
3218
|
+
* into a regular `Response` (same shape as a normal post()).
|
|
3219
|
+
*
|
|
3220
|
+
* @param {string} method HTTP method (POST / PUT / PATCH typically)
|
|
3221
|
+
* @param {string} url
|
|
3222
|
+
* @param {AsyncIterable<Buffer>|Iterable<Buffer>} chunks Body chunks
|
|
3223
|
+
* @param {Object} [options]
|
|
3224
|
+
* @param {Object} [options.headers]
|
|
3225
|
+
* @param {string} [options.contentType="application/octet-stream"]
|
|
3226
|
+
* @param {number} [options.timeout] Per-request timeout in ms
|
|
3227
|
+
* @returns {Promise<Response>}
|
|
3228
|
+
*/
|
|
3229
|
+
async uploadStream(method, url, chunks, options = {}) {
|
|
3230
|
+
const { headers = null, contentType = "application/octet-stream", timeout = null } = options;
|
|
3231
|
+
const mergedHeaders = this._mergeHeaders(headers) || {};
|
|
3232
|
+
const optsJson = JSON.stringify({
|
|
3233
|
+
method: method.toUpperCase(),
|
|
3234
|
+
headers: mergedHeaders,
|
|
3235
|
+
content_type: contentType,
|
|
3236
|
+
...(timeout ? { timeout } : {}),
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3239
|
+
const uploadHandle = this._lib.httpcloak_upload_start(this._handle, url, optsJson);
|
|
3240
|
+
if (uploadHandle <= 0) {
|
|
3241
|
+
throw new HTTPCloakError("Failed to start streaming upload");
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
const startTime = Date.now();
|
|
3245
|
+
try {
|
|
3246
|
+
for await (const chunk of chunks) {
|
|
3247
|
+
const buf = Buffer.isBuffer(chunk)
|
|
3248
|
+
? chunk
|
|
3249
|
+
: typeof chunk === "string"
|
|
3250
|
+
? Buffer.from(chunk, "utf8")
|
|
3251
|
+
: Buffer.from(chunk);
|
|
3252
|
+
if (buf.length === 0) continue;
|
|
3253
|
+
const written = this._lib.httpcloak_upload_write_raw(uploadHandle, buf, buf.length);
|
|
3254
|
+
if (written < 0) {
|
|
3255
|
+
throw new HTTPCloakError("Failed to write upload chunk");
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
const resultPtr = this._lib.httpcloak_upload_finish(uploadHandle);
|
|
3260
|
+
const elapsed = Date.now() - startTime;
|
|
3261
|
+
const raw = resultToString(resultPtr);
|
|
3262
|
+
if (!raw) {
|
|
3263
|
+
throw new HTTPCloakError("Empty response from streaming upload");
|
|
3264
|
+
}
|
|
3265
|
+
const data = JSON.parse(raw);
|
|
3266
|
+
if (data.error) {
|
|
3267
|
+
throw new HTTPCloakError(data.error);
|
|
3268
|
+
}
|
|
3269
|
+
return new Response(data, elapsed);
|
|
3270
|
+
} catch (err) {
|
|
3271
|
+
try { this._lib.httpcloak_upload_cancel(uploadHandle); } catch { /* best-effort */ }
|
|
3272
|
+
throw err;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
/**
|
|
3277
|
+
* Convenience wrapper: streaming POST. Same semantics as
|
|
3278
|
+
* {@link uploadStream}('POST', ...).
|
|
3279
|
+
*/
|
|
3280
|
+
postUpload(url, chunks, options = {}) {
|
|
3281
|
+
return this.uploadStream("POST", url, chunks, options);
|
|
3282
|
+
}
|
|
2897
3283
|
}
|
|
2898
3284
|
|
|
2899
3285
|
// =============================================================================
|
|
@@ -3227,6 +3613,41 @@ class LocalProxy {
|
|
|
3227
3613
|
return result === 1;
|
|
3228
3614
|
}
|
|
3229
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* Return the IDs of every session currently registered on this proxy.
|
|
3618
|
+
*
|
|
3619
|
+
* These are the same IDs the X-HTTPCloak-Session header accepts for
|
|
3620
|
+
* per-request session routing. Useful for sanity checks, GC of stale
|
|
3621
|
+
* registrations in long-running processes, and operational dashboards.
|
|
3622
|
+
*
|
|
3623
|
+
* @returns {string[]} List of registered session IDs (empty if none).
|
|
3624
|
+
*/
|
|
3625
|
+
listSessions() {
|
|
3626
|
+
const resultPtr = this._lib.httpcloak_local_proxy_list_sessions(this._handle);
|
|
3627
|
+
const raw = resultToString(resultPtr);
|
|
3628
|
+
if (!raw) return [];
|
|
3629
|
+
try {
|
|
3630
|
+
const parsed = JSON.parse(raw);
|
|
3631
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
3632
|
+
} catch {
|
|
3633
|
+
return [];
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
/**
|
|
3638
|
+
* Return true if a session with the given ID is currently registered.
|
|
3639
|
+
*
|
|
3640
|
+
* Cheaper than listSessions() + .includes() when callers only need an
|
|
3641
|
+
* existence check.
|
|
3642
|
+
*
|
|
3643
|
+
* @param {string} sessionId
|
|
3644
|
+
* @returns {boolean}
|
|
3645
|
+
*/
|
|
3646
|
+
hasSession(sessionId) {
|
|
3647
|
+
if (!sessionId) return false;
|
|
3648
|
+
return this._lib.httpcloak_local_proxy_has_session(this._handle, sessionId) === 1;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3230
3651
|
/**
|
|
3231
3652
|
* Stop the local proxy server.
|
|
3232
3653
|
*/
|
package/lib/index.mjs
CHANGED
|
@@ -22,10 +22,17 @@ export const Cookie = cjs.Cookie;
|
|
|
22
22
|
export const RedirectInfo = cjs.RedirectInfo;
|
|
23
23
|
export const HTTPCloakError = cjs.HTTPCloakError;
|
|
24
24
|
export const SessionCacheBackend = cjs.SessionCacheBackend;
|
|
25
|
+
export const PresetPool = cjs.PresetPool;
|
|
25
26
|
|
|
26
27
|
// Presets
|
|
27
28
|
export const Preset = cjs.Preset;
|
|
28
29
|
|
|
30
|
+
// Custom preset loading
|
|
31
|
+
export const loadPreset = cjs.loadPreset;
|
|
32
|
+
export const loadPresetFromJSON = cjs.loadPresetFromJSON;
|
|
33
|
+
export const unregisterPreset = cjs.unregisterPreset;
|
|
34
|
+
export const describePreset = cjs.describePreset;
|
|
35
|
+
|
|
29
36
|
// Configuration
|
|
30
37
|
export const configure = cjs.configure;
|
|
31
38
|
export const configureSessionCache = cjs.configureSessionCache;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "httpcloak",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
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/darwin-arm64": "1.6.
|
|
53
|
-
"@httpcloak/darwin-x64": "1.6.
|
|
54
|
-
"@httpcloak/linux-arm64": "1.6.
|
|
55
|
-
"@httpcloak/linux-x64": "1.6.
|
|
56
|
-
"@httpcloak/win32-x64": "1.6.
|
|
52
|
+
"@httpcloak/darwin-arm64": "1.6.7",
|
|
53
|
+
"@httpcloak/darwin-x64": "1.6.7",
|
|
54
|
+
"@httpcloak/linux-arm64": "1.6.7",
|
|
55
|
+
"@httpcloak/linux-x64": "1.6.7",
|
|
56
|
+
"@httpcloak/win32-x64": "1.6.7"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^25.1.0",
|