httpcloak 1.6.5 → 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 +257 -28
- package/lib/index.js +470 -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 -7
- package/scripts/setup-npm-packages.js +5 -1
- package/npm/win32-arm64/lib.js +0 -3
- package/npm/win32-arm64/lib.mjs +0 -6
- package/npm/win32-arm64/package.json +0 -27
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;
|
|
@@ -241,12 +241,30 @@ export interface SessionOptions {
|
|
|
241
241
|
enableSpeculativeTls?: boolean;
|
|
242
242
|
/** Protocol to switch to after Refresh() (e.g., "h1", "h2", "h3") */
|
|
243
243
|
switchProtocol?: string;
|
|
244
|
+
/** Disable internal cookie jar entirely — caller manages cookies via per-request headers (default: false) */
|
|
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;
|
|
244
252
|
/** Custom JA3 fingerprint string (e.g., "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0") */
|
|
245
253
|
ja3?: string;
|
|
246
254
|
/** Custom Akamai HTTP/2 fingerprint string (e.g., "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p") */
|
|
247
255
|
akamai?: string;
|
|
248
256
|
/** Extra fingerprint options: { tls_alpn, tls_signature_algorithms, tls_cert_compression, tls_permute_extensions } */
|
|
249
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;
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
export interface RequestOptions {
|
|
@@ -281,6 +299,64 @@ export interface RequestOptions {
|
|
|
281
299
|
* Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
|
|
282
300
|
*/
|
|
283
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>;
|
|
284
360
|
}
|
|
285
361
|
|
|
286
362
|
export class Session {
|
|
@@ -311,8 +387,11 @@ export class Session {
|
|
|
311
387
|
/** Refresh the session by closing all connections while keeping TLS session tickets.
|
|
312
388
|
* This simulates a browser page refresh - connections are severed but 0-RTT
|
|
313
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.
|
|
314
393
|
*/
|
|
315
|
-
refresh(): void;
|
|
394
|
+
refresh(switchProtocol?: "h1" | "h2" | "h3"): void;
|
|
316
395
|
|
|
317
396
|
// Synchronous methods
|
|
318
397
|
/** Perform a synchronous GET request */
|
|
@@ -357,17 +436,11 @@ export class Session {
|
|
|
357
436
|
/** Get a specific cookie by name with full metadata */
|
|
358
437
|
getCookieDetailed(name: string): Cookie | null;
|
|
359
438
|
|
|
360
|
-
/**
|
|
361
|
-
|
|
362
|
-
* @deprecated Will return Cookie[] with full metadata in a future release. Use getCookiesDetailed() for the new format now.
|
|
363
|
-
*/
|
|
364
|
-
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[];
|
|
365
441
|
|
|
366
|
-
/**
|
|
367
|
-
|
|
368
|
-
* @deprecated Will return Cookie|null in a future release. Use getCookieDetailed() for the new format now.
|
|
369
|
-
*/
|
|
370
|
-
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;
|
|
371
444
|
|
|
372
445
|
/** Set a cookie in the session */
|
|
373
446
|
setCookie(
|
|
@@ -390,11 +463,72 @@ export class Session {
|
|
|
390
463
|
/** Clear all cookies from the session */
|
|
391
464
|
clearCookies(): void;
|
|
392
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
|
+
|
|
393
524
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
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.
|
|
396
527
|
*/
|
|
397
|
-
|
|
528
|
+
setMaxRedirects(max: number): void;
|
|
529
|
+
|
|
530
|
+
/** Read the session's current redirect cap. */
|
|
531
|
+
getMaxRedirects(): number;
|
|
398
532
|
|
|
399
533
|
// Proxy management
|
|
400
534
|
|
|
@@ -655,6 +789,37 @@ export class Session {
|
|
|
655
789
|
* @returns FastResponse with Buffer body
|
|
656
790
|
*/
|
|
657
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>;
|
|
658
823
|
}
|
|
659
824
|
|
|
660
825
|
export interface LocalProxyOptions {
|
|
@@ -675,16 +840,20 @@ export interface LocalProxyOptions {
|
|
|
675
840
|
}
|
|
676
841
|
|
|
677
842
|
export interface LocalProxyStats {
|
|
678
|
-
/**
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
|
|
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;
|
|
688
857
|
}
|
|
689
858
|
|
|
690
859
|
/**
|
|
@@ -788,6 +957,22 @@ export class LocalProxy {
|
|
|
788
957
|
*/
|
|
789
958
|
unregisterSession(sessionId: string): boolean;
|
|
790
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
|
+
|
|
791
976
|
/**
|
|
792
977
|
* Stop and close the proxy.
|
|
793
978
|
* After closing, the LocalProxy instance cannot be reused.
|
|
@@ -798,8 +983,30 @@ export class LocalProxy {
|
|
|
798
983
|
/** Get the httpcloak library version */
|
|
799
984
|
export function version(): string;
|
|
800
985
|
|
|
801
|
-
/**
|
|
802
|
-
|
|
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;
|
|
803
1010
|
|
|
804
1011
|
/**
|
|
805
1012
|
* Configure the DNS servers used for ECH (Encrypted Client Hello) config queries.
|
|
@@ -857,6 +1064,28 @@ export function request(method: string, url: string, options?: RequestOptions):
|
|
|
857
1064
|
|
|
858
1065
|
/** Available browser presets */
|
|
859
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;
|
|
860
1089
|
CHROME_146: string;
|
|
861
1090
|
CHROME_146_WINDOWS: string;
|
|
862
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
|
}
|
|
@@ -1361,6 +1474,10 @@ class Session {
|
|
|
1361
1474
|
keyLogFile = null,
|
|
1362
1475
|
enableSpeculativeTls = false,
|
|
1363
1476
|
switchProtocol = null,
|
|
1477
|
+
withoutCookieJar = false,
|
|
1478
|
+
withoutConditionalCache = false,
|
|
1479
|
+
disableEch = false,
|
|
1480
|
+
disableHttp3 = false,
|
|
1364
1481
|
ja3 = null,
|
|
1365
1482
|
akamai = null,
|
|
1366
1483
|
extraFp = null,
|
|
@@ -1435,6 +1552,18 @@ class Session {
|
|
|
1435
1552
|
if (switchProtocol) {
|
|
1436
1553
|
config.switch_protocol = switchProtocol;
|
|
1437
1554
|
}
|
|
1555
|
+
if (withoutCookieJar) {
|
|
1556
|
+
config.without_cookie_jar = true;
|
|
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
|
+
}
|
|
1438
1567
|
if (ja3) {
|
|
1439
1568
|
config.ja3 = ja3;
|
|
1440
1569
|
}
|
|
@@ -1598,7 +1727,7 @@ class Session {
|
|
|
1598
1727
|
* @returns {Response} Response object
|
|
1599
1728
|
*/
|
|
1600
1729
|
getSync(url, options = {}) {
|
|
1601
|
-
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;
|
|
1602
1731
|
|
|
1603
1732
|
url = addParamsToUrl(url, params);
|
|
1604
1733
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1615,6 +1744,12 @@ class Session {
|
|
|
1615
1744
|
if (fetchMode) {
|
|
1616
1745
|
reqOptions.fetch_mode = fetchMode;
|
|
1617
1746
|
}
|
|
1747
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1748
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1749
|
+
}
|
|
1750
|
+
if (disableConditionalCache) {
|
|
1751
|
+
reqOptions.disable_conditional_cache = true;
|
|
1752
|
+
}
|
|
1618
1753
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1619
1754
|
|
|
1620
1755
|
const startTime = Date.now();
|
|
@@ -1644,7 +1779,7 @@ class Session {
|
|
|
1644
1779
|
* @returns {Response} Response object
|
|
1645
1780
|
*/
|
|
1646
1781
|
postSync(url, options = {}) {
|
|
1647
|
-
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;
|
|
1648
1783
|
|
|
1649
1784
|
url = addParamsToUrl(url, params);
|
|
1650
1785
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1689,6 +1824,12 @@ class Session {
|
|
|
1689
1824
|
if (fetchMode) {
|
|
1690
1825
|
reqOptions.fetch_mode = fetchMode;
|
|
1691
1826
|
}
|
|
1827
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1828
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1829
|
+
}
|
|
1830
|
+
if (disableConditionalCache) {
|
|
1831
|
+
reqOptions.disable_conditional_cache = true;
|
|
1832
|
+
}
|
|
1692
1833
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1693
1834
|
|
|
1694
1835
|
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
@@ -1713,7 +1854,7 @@ class Session {
|
|
|
1713
1854
|
* @returns {Response} Response object
|
|
1714
1855
|
*/
|
|
1715
1856
|
requestSync(method, url, options = {}) {
|
|
1716
|
-
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;
|
|
1717
1858
|
|
|
1718
1859
|
url = addParamsToUrl(url, params);
|
|
1719
1860
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1756,6 +1897,8 @@ class Session {
|
|
|
1756
1897
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1757
1898
|
if (timeout) requestConfig.timeout = timeout;
|
|
1758
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;
|
|
1759
1902
|
|
|
1760
1903
|
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1761
1904
|
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
@@ -1786,7 +1929,7 @@ class Session {
|
|
|
1786
1929
|
* @returns {Promise<Response>} Response object
|
|
1787
1930
|
*/
|
|
1788
1931
|
get(url, options = {}) {
|
|
1789
|
-
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;
|
|
1790
1933
|
|
|
1791
1934
|
url = addParamsToUrl(url, params);
|
|
1792
1935
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1809,11 +1952,17 @@ class Session {
|
|
|
1809
1952
|
// httpcloak_request_async — that path also uses time.Second).
|
|
1810
1953
|
reqOptions.timeout = timeout;
|
|
1811
1954
|
}
|
|
1955
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1956
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1957
|
+
}
|
|
1958
|
+
if (disableConditionalCache) {
|
|
1959
|
+
reqOptions.disable_conditional_cache = true;
|
|
1960
|
+
}
|
|
1812
1961
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1813
1962
|
|
|
1814
1963
|
// Register async request with callback manager
|
|
1815
1964
|
const manager = getAsyncManager();
|
|
1816
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1965
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1817
1966
|
|
|
1818
1967
|
// Start async request
|
|
1819
1968
|
this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
|
|
@@ -1829,16 +1978,23 @@ class Session {
|
|
|
1829
1978
|
* @returns {Promise<Response>} Response object
|
|
1830
1979
|
*/
|
|
1831
1980
|
post(url, options = {}) {
|
|
1832
|
-
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;
|
|
1833
1982
|
|
|
1834
1983
|
url = addParamsToUrl(url, params);
|
|
1835
1984
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1836
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
|
+
|
|
1837
1992
|
// Handle multipart file upload
|
|
1838
1993
|
if (files !== null) {
|
|
1839
1994
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1840
1995
|
const multipart = encodeMultipart(formData, files);
|
|
1841
|
-
body = multipart.body.toString("
|
|
1996
|
+
body = multipart.body.toString("base64");
|
|
1997
|
+
bodyEncoding = "base64";
|
|
1842
1998
|
mergedHeaders = mergedHeaders || {};
|
|
1843
1999
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1844
2000
|
}
|
|
@@ -1858,9 +2014,11 @@ class Session {
|
|
|
1858
2014
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1859
2015
|
}
|
|
1860
2016
|
}
|
|
1861
|
-
// 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.
|
|
1862
2019
|
else if (Buffer.isBuffer(body)) {
|
|
1863
|
-
body = body.toString("
|
|
2020
|
+
body = body.toString("base64");
|
|
2021
|
+
bodyEncoding = "base64";
|
|
1864
2022
|
}
|
|
1865
2023
|
|
|
1866
2024
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1880,11 +2038,20 @@ class Session {
|
|
|
1880
2038
|
// Public API: seconds. clib post_async path enforces in seconds.
|
|
1881
2039
|
reqOptions.timeout = timeout;
|
|
1882
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
|
+
}
|
|
1883
2050
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1884
2051
|
|
|
1885
2052
|
// Register async request with callback manager
|
|
1886
2053
|
const manager = getAsyncManager();
|
|
1887
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
2054
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1888
2055
|
|
|
1889
2056
|
// Start async request
|
|
1890
2057
|
this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
|
|
@@ -1901,16 +2068,22 @@ class Session {
|
|
|
1901
2068
|
* @returns {Promise<Response>} Response object
|
|
1902
2069
|
*/
|
|
1903
2070
|
request(method, url, options = {}) {
|
|
1904
|
-
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;
|
|
1905
2072
|
|
|
1906
2073
|
url = addParamsToUrl(url, params);
|
|
1907
2074
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1908
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
|
+
|
|
1909
2081
|
// Handle multipart file upload
|
|
1910
2082
|
if (files !== null) {
|
|
1911
2083
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1912
2084
|
const multipart = encodeMultipart(formData, files);
|
|
1913
|
-
body = multipart.body.toString("
|
|
2085
|
+
body = multipart.body.toString("base64");
|
|
2086
|
+
bodyEncoding = "base64";
|
|
1914
2087
|
mergedHeaders = mergedHeaders || {};
|
|
1915
2088
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1916
2089
|
}
|
|
@@ -1930,9 +2103,11 @@ class Session {
|
|
|
1930
2103
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1931
2104
|
}
|
|
1932
2105
|
}
|
|
1933
|
-
// 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.
|
|
1934
2108
|
else if (Buffer.isBuffer(body)) {
|
|
1935
|
-
body = body.toString("
|
|
2109
|
+
body = body.toString("base64");
|
|
2110
|
+
bodyEncoding = "base64";
|
|
1936
2111
|
}
|
|
1937
2112
|
|
|
1938
2113
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1946,12 +2121,15 @@ class Session {
|
|
|
1946
2121
|
};
|
|
1947
2122
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1948
2123
|
if (body) requestConfig.body = body;
|
|
2124
|
+
if (bodyEncoding) requestConfig.body_encoding = bodyEncoding;
|
|
1949
2125
|
if (timeout) requestConfig.timeout = timeout;
|
|
1950
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;
|
|
1951
2129
|
|
|
1952
2130
|
// Register async request with callback manager
|
|
1953
2131
|
const manager = getAsyncManager();
|
|
1954
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
2132
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1955
2133
|
|
|
1956
2134
|
// Start async request
|
|
1957
2135
|
this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
|
|
@@ -2099,6 +2277,122 @@ class Session {
|
|
|
2099
2277
|
return this.getCookies();
|
|
2100
2278
|
}
|
|
2101
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
|
+
|
|
2102
2396
|
// ===========================================================================
|
|
2103
2397
|
// Proxy Management
|
|
2104
2398
|
// ===========================================================================
|
|
@@ -2395,7 +2689,7 @@ class Session {
|
|
|
2395
2689
|
* stream.close();
|
|
2396
2690
|
*/
|
|
2397
2691
|
getStream(url, options = {}) {
|
|
2398
|
-
const { params, headers, cookies, timeout } = options;
|
|
2692
|
+
const { params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
|
|
2399
2693
|
|
|
2400
2694
|
// Add params to URL
|
|
2401
2695
|
if (params) {
|
|
@@ -2422,6 +2716,12 @@ class Session {
|
|
|
2422
2716
|
if (timeout) {
|
|
2423
2717
|
reqOptions.timeout = timeout;
|
|
2424
2718
|
}
|
|
2719
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2720
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2721
|
+
}
|
|
2722
|
+
if (disableConditionalCache) {
|
|
2723
|
+
reqOptions.disable_conditional_cache = true;
|
|
2724
|
+
}
|
|
2425
2725
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2426
2726
|
|
|
2427
2727
|
// Start stream
|
|
@@ -2461,7 +2761,7 @@ class Session {
|
|
|
2461
2761
|
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
2462
2762
|
*/
|
|
2463
2763
|
postStream(url, options = {}) {
|
|
2464
|
-
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;
|
|
2465
2765
|
|
|
2466
2766
|
// Add params to URL
|
|
2467
2767
|
if (params) {
|
|
@@ -2501,6 +2801,12 @@ class Session {
|
|
|
2501
2801
|
if (timeout) {
|
|
2502
2802
|
reqOptions.timeout = timeout;
|
|
2503
2803
|
}
|
|
2804
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2805
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2806
|
+
}
|
|
2807
|
+
if (disableConditionalCache) {
|
|
2808
|
+
reqOptions.disable_conditional_cache = true;
|
|
2809
|
+
}
|
|
2504
2810
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2505
2811
|
|
|
2506
2812
|
// Start stream
|
|
@@ -2539,7 +2845,7 @@ class Session {
|
|
|
2539
2845
|
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
2540
2846
|
*/
|
|
2541
2847
|
requestStream(method, url, options = {}) {
|
|
2542
|
-
const { body, params, headers, cookies, timeout } = options;
|
|
2848
|
+
const { body, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false } = options;
|
|
2543
2849
|
|
|
2544
2850
|
// Add params to URL
|
|
2545
2851
|
if (params) {
|
|
@@ -2572,6 +2878,12 @@ class Session {
|
|
|
2572
2878
|
if (timeout) {
|
|
2573
2879
|
requestConfig.timeout = timeout;
|
|
2574
2880
|
}
|
|
2881
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2882
|
+
requestConfig.follow_redirects = !!allowRedirects;
|
|
2883
|
+
}
|
|
2884
|
+
if (disableConditionalCache) {
|
|
2885
|
+
requestConfig.disable_conditional_cache = true;
|
|
2886
|
+
}
|
|
2575
2887
|
|
|
2576
2888
|
// Start stream
|
|
2577
2889
|
const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
|
|
@@ -2890,6 +3202,84 @@ class Session {
|
|
|
2890
3202
|
patchFast(url, options = {}) {
|
|
2891
3203
|
return this.requestFast("PATCH", url, options);
|
|
2892
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
|
+
}
|
|
2893
3283
|
}
|
|
2894
3284
|
|
|
2895
3285
|
// =============================================================================
|
|
@@ -3223,6 +3613,41 @@ class LocalProxy {
|
|
|
3223
3613
|
return result === 1;
|
|
3224
3614
|
}
|
|
3225
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
|
+
|
|
3226
3651
|
/**
|
|
3227
3652
|
* Stop the local proxy server.
|
|
3228
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,12 +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-
|
|
57
|
-
"@httpcloak/win32-x64": "1.6.5"
|
|
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"
|
|
58
57
|
},
|
|
59
58
|
"devDependencies": {
|
|
60
59
|
"@types/node": "^25.1.0",
|
|
@@ -8,13 +8,17 @@ const path = require("path");
|
|
|
8
8
|
|
|
9
9
|
const VERSION = "1.4.0";
|
|
10
10
|
|
|
11
|
+
// Platforms we actually build and publish via CI's npm-platform matrix.
|
|
12
|
+
// Keep in sync with .github/workflows/bindings.yml's `npm_platform` matrix —
|
|
13
|
+
// adding a row here without adding it to the matrix means npm install will
|
|
14
|
+
// fail with an unresolvable optional dependency on yarn classic / pnpm
|
|
15
|
+
// strict modes.
|
|
11
16
|
const PLATFORMS = [
|
|
12
17
|
{ name: "linux-x64", os: "linux", cpu: "x64", libName: "libhttpcloak-linux-amd64.so" },
|
|
13
18
|
{ name: "linux-arm64", os: "linux", cpu: "arm64", libName: "libhttpcloak-linux-arm64.so" },
|
|
14
19
|
{ name: "darwin-x64", os: "darwin", cpu: "x64", libName: "libhttpcloak-darwin-amd64.dylib" },
|
|
15
20
|
{ name: "darwin-arm64", os: "darwin", cpu: "arm64", libName: "libhttpcloak-darwin-arm64.dylib" },
|
|
16
21
|
{ name: "win32-x64", os: "win32", cpu: "x64", libName: "libhttpcloak-windows-amd64.dll" },
|
|
17
|
-
{ name: "win32-arm64", os: "win32", cpu: "arm64", libName: "libhttpcloak-windows-arm64.dll" },
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
const npmDir = path.join(__dirname, "..", "npm");
|
package/npm/win32-arm64/lib.js
DELETED
package/npm/win32-arm64/lib.mjs
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@httpcloak/win32-arm64",
|
|
3
|
-
"version": "1.6.5",
|
|
4
|
-
"description": "HTTPCloak native binary for win32 arm64",
|
|
5
|
-
"os": [
|
|
6
|
-
"win32"
|
|
7
|
-
],
|
|
8
|
-
"cpu": [
|
|
9
|
-
"arm64"
|
|
10
|
-
],
|
|
11
|
-
"main": "lib.js",
|
|
12
|
-
"module": "lib.mjs",
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"import": "./lib.mjs",
|
|
16
|
-
"require": "./lib.js"
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "https://github.com/sardanioss/httpcloak"
|
|
23
|
-
},
|
|
24
|
-
"publishConfig": {
|
|
25
|
-
"access": "public"
|
|
26
|
-
}
|
|
27
|
-
}
|