httpcloak 1.6.1 → 1.6.6
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 +78 -0
- package/lib/index.js +451 -111
- 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
|
@@ -241,6 +241,8 @@ 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;
|
|
244
246
|
/** Custom JA3 fingerprint string (e.g., "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0") */
|
|
245
247
|
ja3?: string;
|
|
246
248
|
/** Custom Akamai HTTP/2 fingerprint string (e.g., "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p") */
|
|
@@ -268,6 +270,19 @@ export interface RequestOptions {
|
|
|
268
270
|
auth?: [string, string];
|
|
269
271
|
/** Optional request timeout in seconds */
|
|
270
272
|
timeout?: number;
|
|
273
|
+
/**
|
|
274
|
+
* Explicit Sec-Fetch-Mode/Dest override for requests where auto-sniffing isn't enough.
|
|
275
|
+
*
|
|
276
|
+
* Valid values:
|
|
277
|
+
* - `"cors"` - XHR/fetch() request (Sec-Fetch-Mode: cors, Sec-Fetch-Dest: empty, Sec-Fetch-Site: same-origin)
|
|
278
|
+
* - `"no-cors"` - Subresource load (image/script/stylesheet tag)
|
|
279
|
+
* - `"navigate"` - Top-level navigation (document load, classic form POST)
|
|
280
|
+
* - `"websocket"` - WebSocket upgrade
|
|
281
|
+
*
|
|
282
|
+
* When unset (default), httpcloak auto-detects based on method, Accept, Content-Type, and Sec-Fetch-Dest headers.
|
|
283
|
+
* Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
|
|
284
|
+
*/
|
|
285
|
+
fetchMode?: "cors" | "no-cors" | "navigate" | "websocket";
|
|
271
286
|
}
|
|
272
287
|
|
|
273
288
|
export class Session {
|
|
@@ -1020,3 +1035,66 @@ export function configureSessionCache(options: SessionCacheOptions): SessionCach
|
|
|
1020
1035
|
* After calling this, new sessions will not use distributed caching.
|
|
1021
1036
|
*/
|
|
1022
1037
|
export function clearSessionCache(): void;
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Load a custom preset from a JSON file and register it.
|
|
1041
|
+
* @param filePath - Path to the preset JSON file
|
|
1042
|
+
* @returns The registered preset name
|
|
1043
|
+
*/
|
|
1044
|
+
export function loadPreset(filePath: string): string;
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Load a custom preset from a JSON string and register it.
|
|
1048
|
+
* @param jsonData - JSON string defining the preset
|
|
1049
|
+
* @returns The registered preset name
|
|
1050
|
+
*/
|
|
1051
|
+
export function loadPresetFromJSON(jsonData: string): string;
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Unregister a custom preset by name.
|
|
1055
|
+
* @param name - The preset name to unregister
|
|
1056
|
+
*/
|
|
1057
|
+
export function unregisterPreset(name: string): void;
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* A pool of custom fingerprint presets for rotation.
|
|
1061
|
+
*
|
|
1062
|
+
* Pools load multiple presets from a single JSON file and provide
|
|
1063
|
+
* round-robin or random selection. All presets are auto-registered
|
|
1064
|
+
* on construction, so you can pass the returned name directly to
|
|
1065
|
+
* `new Session({ preset: name })`.
|
|
1066
|
+
*/
|
|
1067
|
+
export class PresetPool {
|
|
1068
|
+
/**
|
|
1069
|
+
* Load a preset pool from a JSON file.
|
|
1070
|
+
* @param filePath - Path to the pool JSON file
|
|
1071
|
+
*/
|
|
1072
|
+
constructor(filePath: string);
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Load a preset pool from a JSON string.
|
|
1076
|
+
* @param jsonData - JSON string defining the pool
|
|
1077
|
+
*/
|
|
1078
|
+
static fromJSON(jsonData: string): PresetPool;
|
|
1079
|
+
|
|
1080
|
+
/** Pick a preset using the pool's configured strategy. */
|
|
1081
|
+
pick(): string;
|
|
1082
|
+
|
|
1083
|
+
/** Pick a random preset from the pool. */
|
|
1084
|
+
random(): string;
|
|
1085
|
+
|
|
1086
|
+
/** Pick the next preset in round-robin order. */
|
|
1087
|
+
next(): string;
|
|
1088
|
+
|
|
1089
|
+
/** Get a preset by index. */
|
|
1090
|
+
get(index: number): string;
|
|
1091
|
+
|
|
1092
|
+
/** Number of presets in the pool. */
|
|
1093
|
+
readonly size: number;
|
|
1094
|
+
|
|
1095
|
+
/** Name of the preset pool. */
|
|
1096
|
+
readonly name: string;
|
|
1097
|
+
|
|
1098
|
+
/** Free the pool handle and unregister all its presets. */
|
|
1099
|
+
close(): void;
|
|
1100
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -216,11 +216,28 @@ class Response {
|
|
|
216
216
|
* @param {Object} data - Response data from native library
|
|
217
217
|
* @param {number} [elapsed=0] - Elapsed time in milliseconds
|
|
218
218
|
*/
|
|
219
|
-
constructor(data, elapsed = 0) {
|
|
219
|
+
constructor(data, elapsed = 0, rawBody = null) {
|
|
220
220
|
this.statusCode = data.status_code || 0;
|
|
221
221
|
this.headers = data.headers || {};
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
if (rawBody !== null) {
|
|
223
|
+
// Raw-path: body arrived as binary bytes alongside metadata JSON
|
|
224
|
+
// (httpcloak_*_raw + response_finalize). No base64 round trip — fastest
|
|
225
|
+
// and loss-free for PDFs/images/etc.
|
|
226
|
+
this._body = rawBody;
|
|
227
|
+
this._text = rawBody.toString("utf8"); // best-effort text view
|
|
228
|
+
} else {
|
|
229
|
+
// JSON path: body is a string inside the JSON blob. Non-UTF-8 bodies
|
|
230
|
+
// arrive base64-encoded (Go side since 98e4228). Valid UTF-8 passes
|
|
231
|
+
// through as plain text.
|
|
232
|
+
const bodyStr = data.body || "";
|
|
233
|
+
if (data.body_encoding === "base64") {
|
|
234
|
+
this._body = Buffer.from(bodyStr, "base64");
|
|
235
|
+
this._text = this._body.toString("utf8");
|
|
236
|
+
} else {
|
|
237
|
+
this._body = Buffer.from(bodyStr, "utf8");
|
|
238
|
+
this._text = bodyStr;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
224
241
|
this.finalUrl = data.final_url || "";
|
|
225
242
|
this.protocol = data.protocol || "";
|
|
226
243
|
this.elapsed = elapsed; // milliseconds
|
|
@@ -787,27 +804,37 @@ function getLib() {
|
|
|
787
804
|
const libPath = getLibPath();
|
|
788
805
|
nativeLibHandle = koffi.load(libPath);
|
|
789
806
|
|
|
790
|
-
//
|
|
791
|
-
//
|
|
807
|
+
// Declare the free function first so we can hand it to koffi.disposable.
|
|
808
|
+
// httpcloak_free_string is NULL-safe on the Go side (no-op on nil).
|
|
809
|
+
const httpcloakFreeString = nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]);
|
|
810
|
+
|
|
811
|
+
// Named disposable type derived from "str": koffi decodes the C string
|
|
812
|
+
// into a JS string AND then calls httpcloakFreeString on the original
|
|
813
|
+
// malloc'd pointer. Every function below that returned plain "str"
|
|
814
|
+
// previously leaked that malloc (issue #48: customers reported ~48MB/h
|
|
815
|
+
// RSS growth in sustained production traffic). Using HeapStr closes the
|
|
816
|
+
// leak without any call-site changes.
|
|
817
|
+
const HeapStr = koffi.disposable("HeapStr", "str", httpcloakFreeString);
|
|
818
|
+
|
|
792
819
|
lib = {
|
|
793
820
|
httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
|
|
794
821
|
httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
|
|
795
822
|
httpcloak_session_refresh: nativeLibHandle.func("httpcloak_session_refresh", "void", ["int64"]),
|
|
796
|
-
httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol",
|
|
797
|
-
httpcloak_session_warmup: nativeLibHandle.func("httpcloak_session_warmup",
|
|
823
|
+
httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol", HeapStr, ["int64", "str"]),
|
|
824
|
+
httpcloak_session_warmup: nativeLibHandle.func("httpcloak_session_warmup", HeapStr, ["int64", "str", "int64"]),
|
|
798
825
|
httpcloak_session_fork: nativeLibHandle.func("httpcloak_session_fork", "int64", ["int64"]),
|
|
799
|
-
httpcloak_get: nativeLibHandle.func("httpcloak_get",
|
|
800
|
-
httpcloak_post: nativeLibHandle.func("httpcloak_post",
|
|
801
|
-
httpcloak_request: nativeLibHandle.func("httpcloak_request",
|
|
802
|
-
httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies",
|
|
826
|
+
httpcloak_get: nativeLibHandle.func("httpcloak_get", HeapStr, ["int64", "str", "str"]),
|
|
827
|
+
httpcloak_post: nativeLibHandle.func("httpcloak_post", HeapStr, ["int64", "str", "str", "str"]),
|
|
828
|
+
httpcloak_request: nativeLibHandle.func("httpcloak_request", HeapStr, ["int64", "str"]),
|
|
829
|
+
httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", HeapStr, ["int64"]),
|
|
803
830
|
httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
|
|
804
831
|
httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
|
|
805
832
|
httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
|
|
806
|
-
httpcloak_free_string:
|
|
807
|
-
httpcloak_version: nativeLibHandle.func("httpcloak_version",
|
|
808
|
-
httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets",
|
|
809
|
-
httpcloak_set_ech_dns_servers: nativeLibHandle.func("httpcloak_set_ech_dns_servers",
|
|
810
|
-
httpcloak_get_ech_dns_servers: nativeLibHandle.func("httpcloak_get_ech_dns_servers",
|
|
833
|
+
httpcloak_free_string: httpcloakFreeString,
|
|
834
|
+
httpcloak_version: nativeLibHandle.func("httpcloak_version", HeapStr, []),
|
|
835
|
+
httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", HeapStr, []),
|
|
836
|
+
httpcloak_set_ech_dns_servers: nativeLibHandle.func("httpcloak_set_ech_dns_servers", HeapStr, ["str"]),
|
|
837
|
+
httpcloak_get_ech_dns_servers: nativeLibHandle.func("httpcloak_get_ech_dns_servers", HeapStr, []),
|
|
811
838
|
// Async functions
|
|
812
839
|
httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
|
|
813
840
|
httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
|
|
@@ -818,41 +845,41 @@ function getLib() {
|
|
|
818
845
|
httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
|
|
819
846
|
httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
|
|
820
847
|
httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
|
|
821
|
-
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata",
|
|
822
|
-
httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read",
|
|
848
|
+
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", HeapStr, ["int64"]),
|
|
849
|
+
httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", HeapStr, ["int64", "int64"]),
|
|
823
850
|
httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
|
|
824
851
|
// Raw response functions for fast-path (zero-copy)
|
|
825
852
|
httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
|
|
826
853
|
httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
|
|
827
854
|
httpcloak_request_raw: nativeLibHandle.func("httpcloak_request_raw", "int64", ["int64", "str", "void*", "int"]),
|
|
828
|
-
httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata",
|
|
855
|
+
httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", HeapStr, ["int64"]),
|
|
829
856
|
httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
|
|
830
857
|
httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
|
|
831
858
|
httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
|
|
832
859
|
// Combined finalize function (copy + metadata + free in one call)
|
|
833
|
-
httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize",
|
|
860
|
+
httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", HeapStr, ["int64", "void*", "int"]),
|
|
834
861
|
// Session persistence functions
|
|
835
|
-
httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save",
|
|
862
|
+
httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", HeapStr, ["int64", "str"]),
|
|
836
863
|
httpcloak_session_load: nativeLibHandle.func("httpcloak_session_load", "int64", ["str"]),
|
|
837
|
-
httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal",
|
|
864
|
+
httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", HeapStr, ["int64"]),
|
|
838
865
|
httpcloak_session_unmarshal: nativeLibHandle.func("httpcloak_session_unmarshal", "int64", ["str"]),
|
|
839
866
|
// Proxy management functions
|
|
840
|
-
httpcloak_session_set_proxy: nativeLibHandle.func("httpcloak_session_set_proxy",
|
|
841
|
-
httpcloak_session_set_tcp_proxy: nativeLibHandle.func("httpcloak_session_set_tcp_proxy",
|
|
842
|
-
httpcloak_session_set_udp_proxy: nativeLibHandle.func("httpcloak_session_set_udp_proxy",
|
|
843
|
-
httpcloak_session_get_proxy: nativeLibHandle.func("httpcloak_session_get_proxy",
|
|
844
|
-
httpcloak_session_get_tcp_proxy: nativeLibHandle.func("httpcloak_session_get_tcp_proxy",
|
|
845
|
-
httpcloak_session_get_udp_proxy: nativeLibHandle.func("httpcloak_session_get_udp_proxy",
|
|
867
|
+
httpcloak_session_set_proxy: nativeLibHandle.func("httpcloak_session_set_proxy", HeapStr, ["int64", "str"]),
|
|
868
|
+
httpcloak_session_set_tcp_proxy: nativeLibHandle.func("httpcloak_session_set_tcp_proxy", HeapStr, ["int64", "str"]),
|
|
869
|
+
httpcloak_session_set_udp_proxy: nativeLibHandle.func("httpcloak_session_set_udp_proxy", HeapStr, ["int64", "str"]),
|
|
870
|
+
httpcloak_session_get_proxy: nativeLibHandle.func("httpcloak_session_get_proxy", HeapStr, ["int64"]),
|
|
871
|
+
httpcloak_session_get_tcp_proxy: nativeLibHandle.func("httpcloak_session_get_tcp_proxy", HeapStr, ["int64"]),
|
|
872
|
+
httpcloak_session_get_udp_proxy: nativeLibHandle.func("httpcloak_session_get_udp_proxy", HeapStr, ["int64"]),
|
|
846
873
|
// Header order customization
|
|
847
|
-
httpcloak_session_set_header_order: nativeLibHandle.func("httpcloak_session_set_header_order",
|
|
848
|
-
httpcloak_session_get_header_order: nativeLibHandle.func("httpcloak_session_get_header_order",
|
|
874
|
+
httpcloak_session_set_header_order: nativeLibHandle.func("httpcloak_session_set_header_order", HeapStr, ["int64", "str"]),
|
|
875
|
+
httpcloak_session_get_header_order: nativeLibHandle.func("httpcloak_session_get_header_order", HeapStr, ["int64"]),
|
|
849
876
|
// Local proxy functions
|
|
850
877
|
httpcloak_local_proxy_start: nativeLibHandle.func("httpcloak_local_proxy_start", "int64", ["str"]),
|
|
851
878
|
httpcloak_local_proxy_stop: nativeLibHandle.func("httpcloak_local_proxy_stop", "void", ["int64"]),
|
|
852
879
|
httpcloak_local_proxy_get_port: nativeLibHandle.func("httpcloak_local_proxy_get_port", "int", ["int64"]),
|
|
853
880
|
httpcloak_local_proxy_is_running: nativeLibHandle.func("httpcloak_local_proxy_is_running", "int", ["int64"]),
|
|
854
|
-
httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats",
|
|
855
|
-
httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session",
|
|
881
|
+
httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", HeapStr, ["int64"]),
|
|
882
|
+
httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", HeapStr, ["int64", "str", "int64"]),
|
|
856
883
|
httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
|
|
857
884
|
// Session cache callbacks
|
|
858
885
|
httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
|
|
@@ -876,6 +903,23 @@ function getLib() {
|
|
|
876
903
|
// Async cache result functions (called by JS to provide results to Go)
|
|
877
904
|
httpcloak_async_cache_get_result: nativeLibHandle.func("httpcloak_async_cache_get_result", "void", ["int64", "str"]),
|
|
878
905
|
httpcloak_async_cache_op_result: nativeLibHandle.func("httpcloak_async_cache_op_result", "void", ["int64", "int"]),
|
|
906
|
+
// Custom preset loading (void* returns for manual free)
|
|
907
|
+
httpcloak_preset_load_file: nativeLibHandle.func("httpcloak_preset_load_file", "void*", ["str"]),
|
|
908
|
+
httpcloak_preset_load_json: nativeLibHandle.func("httpcloak_preset_load_json", "void*", ["str"]),
|
|
909
|
+
httpcloak_preset_unregister: nativeLibHandle.func("httpcloak_preset_unregister", "void", ["str"]),
|
|
910
|
+
// Describe uses HeapStr (issue #48 leak-safe disposable) — koffi
|
|
911
|
+
// auto-frees the malloc'd C string after decode.
|
|
912
|
+
httpcloak_describe_preset: nativeLibHandle.func("httpcloak_describe_preset", HeapStr, ["str"]),
|
|
913
|
+
// Preset pool functions (void* returns for manual free)
|
|
914
|
+
httpcloak_pool_load_file: nativeLibHandle.func("httpcloak_pool_load_file", "void*", ["str"]),
|
|
915
|
+
httpcloak_pool_load_json: nativeLibHandle.func("httpcloak_pool_load_json", "void*", ["str"]),
|
|
916
|
+
httpcloak_pool_pick: nativeLibHandle.func("httpcloak_pool_pick", "void*", ["int64"]),
|
|
917
|
+
httpcloak_pool_random: nativeLibHandle.func("httpcloak_pool_random", "void*", ["int64"]),
|
|
918
|
+
httpcloak_pool_next: nativeLibHandle.func("httpcloak_pool_next", "void*", ["int64"]),
|
|
919
|
+
httpcloak_pool_get: nativeLibHandle.func("httpcloak_pool_get", "void*", ["int64", "int64"]),
|
|
920
|
+
httpcloak_pool_size: nativeLibHandle.func("httpcloak_pool_size", "int64", ["int64"]),
|
|
921
|
+
httpcloak_pool_name: nativeLibHandle.func("httpcloak_pool_name", "void*", ["int64"]),
|
|
922
|
+
httpcloak_pool_free: nativeLibHandle.func("httpcloak_pool_free", "void", ["int64"]),
|
|
879
923
|
};
|
|
880
924
|
}
|
|
881
925
|
return lib;
|
|
@@ -1029,6 +1073,38 @@ function parseResponse(resultPtr, elapsed = 0) {
|
|
|
1029
1073
|
return new Response(data, elapsed);
|
|
1030
1074
|
}
|
|
1031
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* Parse a raw response handle returned by httpcloak_*_raw into a Response
|
|
1078
|
+
* with a binary Buffer body. Avoids the base64 round trip used by the JSON
|
|
1079
|
+
* path — binary bodies (PDFs, images, streams) land in memory once.
|
|
1080
|
+
* @param {Object} lib - Native library binding
|
|
1081
|
+
* @param {number|bigint} responseHandle
|
|
1082
|
+
* @param {number} [elapsed=0] - Elapsed milliseconds
|
|
1083
|
+
* @returns {Response}
|
|
1084
|
+
*/
|
|
1085
|
+
function parseRawResponse(lib, responseHandle, elapsed = 0) {
|
|
1086
|
+
const bodyLen = lib.httpcloak_response_get_body_len(responseHandle);
|
|
1087
|
+
if (bodyLen < 0) {
|
|
1088
|
+
lib.httpcloak_response_free(responseHandle);
|
|
1089
|
+
throw new HTTPCloakError("Failed to get response body length");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const buffer = Buffer.allocUnsafe(bodyLen);
|
|
1093
|
+
// finalize() copies body into buffer, returns metadata JSON (no body), and
|
|
1094
|
+
// frees the handle — a single FFI call instead of three.
|
|
1095
|
+
const metadataStr = lib.httpcloak_response_finalize(responseHandle, buffer, bodyLen);
|
|
1096
|
+
if (!metadataStr) {
|
|
1097
|
+
throw new HTTPCloakError("Failed to finalize response");
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const metadata = JSON.parse(metadataStr);
|
|
1101
|
+
if (metadata.error) {
|
|
1102
|
+
throw new HTTPCloakError(metadata.error);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return new Response(metadata, elapsed, buffer);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1032
1108
|
/**
|
|
1033
1109
|
* Add query parameters to URL
|
|
1034
1110
|
*/
|
|
@@ -1250,8 +1326,8 @@ class Session {
|
|
|
1250
1326
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
1251
1327
|
* @param {boolean} [options.allowRedirects=true] - Follow redirects
|
|
1252
1328
|
* @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
|
|
1253
|
-
* @param {number} [options.retry=
|
|
1254
|
-
* @param {number[]} [options.retryOnStatus] - Status codes to retry on
|
|
1329
|
+
* @param {number} [options.retry=0] - Number of retries on failure (default 0 = no retries; set positive to enable. Issue #57: prior default 3 silently retried POST/PUT/PATCH on 5xx, violating idempotency expectations)
|
|
1330
|
+
* @param {number[]} [options.retryOnStatus] - Status codes to retry on (only consulted when retry > 0)
|
|
1255
1331
|
* @param {number} [options.retryWaitMin=500] - Minimum wait time between retries in milliseconds
|
|
1256
1332
|
* @param {number} [options.retryWaitMax=10000] - Maximum wait time between retries in milliseconds
|
|
1257
1333
|
* @param {Array} [options.auth] - Default auth [username, password] for all requests
|
|
@@ -1271,7 +1347,7 @@ class Session {
|
|
|
1271
1347
|
verify = true,
|
|
1272
1348
|
allowRedirects = true,
|
|
1273
1349
|
maxRedirects = 10,
|
|
1274
|
-
retry =
|
|
1350
|
+
retry = 0,
|
|
1275
1351
|
retryOnStatus = null,
|
|
1276
1352
|
retryWaitMin = 500,
|
|
1277
1353
|
retryWaitMax = 10000,
|
|
@@ -1285,6 +1361,7 @@ class Session {
|
|
|
1285
1361
|
keyLogFile = null,
|
|
1286
1362
|
enableSpeculativeTls = false,
|
|
1287
1363
|
switchProtocol = null,
|
|
1364
|
+
withoutCookieJar = false,
|
|
1288
1365
|
ja3 = null,
|
|
1289
1366
|
akamai = null,
|
|
1290
1367
|
extraFp = null,
|
|
@@ -1359,6 +1436,9 @@ class Session {
|
|
|
1359
1436
|
if (switchProtocol) {
|
|
1360
1437
|
config.switch_protocol = switchProtocol;
|
|
1361
1438
|
}
|
|
1439
|
+
if (withoutCookieJar) {
|
|
1440
|
+
config.without_cookie_jar = true;
|
|
1441
|
+
}
|
|
1362
1442
|
if (ja3) {
|
|
1363
1443
|
config.ja3 = ja3;
|
|
1364
1444
|
}
|
|
@@ -1522,7 +1602,7 @@ class Session {
|
|
|
1522
1602
|
* @returns {Response} Response object
|
|
1523
1603
|
*/
|
|
1524
1604
|
getSync(url, options = {}) {
|
|
1525
|
-
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1605
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1526
1606
|
|
|
1527
1607
|
url = addParamsToUrl(url, params);
|
|
1528
1608
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1536,12 +1616,18 @@ class Session {
|
|
|
1536
1616
|
if (mergedHeaders) {
|
|
1537
1617
|
reqOptions.headers = mergedHeaders;
|
|
1538
1618
|
}
|
|
1619
|
+
if (fetchMode) {
|
|
1620
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1621
|
+
}
|
|
1539
1622
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1540
1623
|
|
|
1541
1624
|
const startTime = Date.now();
|
|
1542
|
-
const
|
|
1625
|
+
const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
|
|
1543
1626
|
const elapsed = Date.now() - startTime;
|
|
1544
|
-
|
|
1627
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1628
|
+
throw new HTTPCloakError("Request failed");
|
|
1629
|
+
}
|
|
1630
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1545
1631
|
}
|
|
1546
1632
|
|
|
1547
1633
|
/**
|
|
@@ -1562,38 +1648,36 @@ class Session {
|
|
|
1562
1648
|
* @returns {Response} Response object
|
|
1563
1649
|
*/
|
|
1564
1650
|
postSync(url, options = {}) {
|
|
1565
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
1651
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1566
1652
|
|
|
1567
1653
|
url = addParamsToUrl(url, params);
|
|
1568
1654
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1569
1655
|
|
|
1570
|
-
//
|
|
1656
|
+
// Normalize body to a Buffer so the raw path can pass (ptr, len) without
|
|
1657
|
+
// null-terminator truncation or utf8 mangling.
|
|
1658
|
+
let bodyBuffer = null;
|
|
1571
1659
|
if (files !== null) {
|
|
1572
1660
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1573
1661
|
const multipart = encodeMultipart(formData, files);
|
|
1574
|
-
|
|
1662
|
+
bodyBuffer = multipart.body;
|
|
1575
1663
|
mergedHeaders = mergedHeaders || {};
|
|
1576
1664
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
else if (json !== null) {
|
|
1580
|
-
body = JSON.stringify(json);
|
|
1665
|
+
} else if (json !== null) {
|
|
1666
|
+
bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
|
|
1581
1667
|
mergedHeaders = mergedHeaders || {};
|
|
1582
1668
|
if (!mergedHeaders["Content-Type"]) {
|
|
1583
1669
|
mergedHeaders["Content-Type"] = "application/json";
|
|
1584
1670
|
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
else if (data !== null && typeof data === "object") {
|
|
1588
|
-
body = new URLSearchParams(data).toString();
|
|
1671
|
+
} else if (data !== null && typeof data === "object") {
|
|
1672
|
+
bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
|
|
1589
1673
|
mergedHeaders = mergedHeaders || {};
|
|
1590
1674
|
if (!mergedHeaders["Content-Type"]) {
|
|
1591
1675
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1592
1676
|
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
else if (
|
|
1596
|
-
|
|
1677
|
+
} else if (Buffer.isBuffer(body)) {
|
|
1678
|
+
bodyBuffer = body;
|
|
1679
|
+
} else if (typeof body === "string") {
|
|
1680
|
+
bodyBuffer = Buffer.from(body, "utf8");
|
|
1597
1681
|
}
|
|
1598
1682
|
|
|
1599
1683
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1606,12 +1690,21 @@ class Session {
|
|
|
1606
1690
|
if (mergedHeaders) {
|
|
1607
1691
|
reqOptions.headers = mergedHeaders;
|
|
1608
1692
|
}
|
|
1693
|
+
if (fetchMode) {
|
|
1694
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1695
|
+
}
|
|
1609
1696
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1610
1697
|
|
|
1698
|
+
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1699
|
+
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
1700
|
+
|
|
1611
1701
|
const startTime = Date.now();
|
|
1612
|
-
const
|
|
1702
|
+
const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, bodyPtr, bodyLen, optionsJson);
|
|
1613
1703
|
const elapsed = Date.now() - startTime;
|
|
1614
|
-
|
|
1704
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1705
|
+
throw new HTTPCloakError("Request failed");
|
|
1706
|
+
}
|
|
1707
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1615
1708
|
}
|
|
1616
1709
|
|
|
1617
1710
|
/**
|
|
@@ -1624,38 +1717,35 @@ class Session {
|
|
|
1624
1717
|
* @returns {Response} Response object
|
|
1625
1718
|
*/
|
|
1626
1719
|
requestSync(method, url, options = {}) {
|
|
1627
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
|
|
1720
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
1628
1721
|
|
|
1629
1722
|
url = addParamsToUrl(url, params);
|
|
1630
1723
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1631
1724
|
|
|
1632
|
-
//
|
|
1725
|
+
// Normalize body to a Buffer so the raw path can pass (ptr, len).
|
|
1726
|
+
let bodyBuffer = null;
|
|
1633
1727
|
if (files !== null) {
|
|
1634
1728
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1635
1729
|
const multipart = encodeMultipart(formData, files);
|
|
1636
|
-
|
|
1730
|
+
bodyBuffer = multipart.body;
|
|
1637
1731
|
mergedHeaders = mergedHeaders || {};
|
|
1638
1732
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
else if (json !== null) {
|
|
1642
|
-
body = JSON.stringify(json);
|
|
1733
|
+
} else if (json !== null) {
|
|
1734
|
+
bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
|
|
1643
1735
|
mergedHeaders = mergedHeaders || {};
|
|
1644
1736
|
if (!mergedHeaders["Content-Type"]) {
|
|
1645
1737
|
mergedHeaders["Content-Type"] = "application/json";
|
|
1646
1738
|
}
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
else if (data !== null && typeof data === "object") {
|
|
1650
|
-
body = new URLSearchParams(data).toString();
|
|
1739
|
+
} else if (data !== null && typeof data === "object") {
|
|
1740
|
+
bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
|
|
1651
1741
|
mergedHeaders = mergedHeaders || {};
|
|
1652
1742
|
if (!mergedHeaders["Content-Type"]) {
|
|
1653
1743
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1654
1744
|
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
else if (
|
|
1658
|
-
|
|
1745
|
+
} else if (Buffer.isBuffer(body)) {
|
|
1746
|
+
bodyBuffer = body;
|
|
1747
|
+
} else if (typeof body === "string") {
|
|
1748
|
+
bodyBuffer = Buffer.from(body, "utf8");
|
|
1659
1749
|
}
|
|
1660
1750
|
|
|
1661
1751
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1668,16 +1758,24 @@ class Session {
|
|
|
1668
1758
|
url,
|
|
1669
1759
|
};
|
|
1670
1760
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1671
|
-
if (body) requestConfig.body = body;
|
|
1672
1761
|
if (timeout) requestConfig.timeout = timeout;
|
|
1762
|
+
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1763
|
+
|
|
1764
|
+
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1765
|
+
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
1673
1766
|
|
|
1674
1767
|
const startTime = Date.now();
|
|
1675
|
-
const
|
|
1768
|
+
const responseHandle = this._lib.httpcloak_request_raw(
|
|
1676
1769
|
this._handle,
|
|
1677
|
-
JSON.stringify(requestConfig)
|
|
1770
|
+
JSON.stringify(requestConfig),
|
|
1771
|
+
bodyPtr,
|
|
1772
|
+
bodyLen
|
|
1678
1773
|
);
|
|
1679
1774
|
const elapsed = Date.now() - startTime;
|
|
1680
|
-
|
|
1775
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1776
|
+
throw new HTTPCloakError("Request failed");
|
|
1777
|
+
}
|
|
1778
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1681
1779
|
}
|
|
1682
1780
|
|
|
1683
1781
|
// ===========================================================================
|
|
@@ -1692,7 +1790,7 @@ class Session {
|
|
|
1692
1790
|
* @returns {Promise<Response>} Response object
|
|
1693
1791
|
*/
|
|
1694
1792
|
get(url, options = {}) {
|
|
1695
|
-
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1793
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1696
1794
|
|
|
1697
1795
|
url = addParamsToUrl(url, params);
|
|
1698
1796
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1706,6 +1804,15 @@ class Session {
|
|
|
1706
1804
|
if (mergedHeaders) {
|
|
1707
1805
|
reqOptions.headers = mergedHeaders;
|
|
1708
1806
|
}
|
|
1807
|
+
if (fetchMode) {
|
|
1808
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1809
|
+
}
|
|
1810
|
+
if (timeout) {
|
|
1811
|
+
// Public API: seconds (matches Session({ timeout }) and the
|
|
1812
|
+
// existing request()/put()/delete()/etc. methods which forward to
|
|
1813
|
+
// httpcloak_request_async — that path also uses time.Second).
|
|
1814
|
+
reqOptions.timeout = timeout;
|
|
1815
|
+
}
|
|
1709
1816
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1710
1817
|
|
|
1711
1818
|
// Register async request with callback manager
|
|
@@ -1726,7 +1833,7 @@ class Session {
|
|
|
1726
1833
|
* @returns {Promise<Response>} Response object
|
|
1727
1834
|
*/
|
|
1728
1835
|
post(url, options = {}) {
|
|
1729
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
1836
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1730
1837
|
|
|
1731
1838
|
url = addParamsToUrl(url, params);
|
|
1732
1839
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1770,6 +1877,13 @@ class Session {
|
|
|
1770
1877
|
if (mergedHeaders) {
|
|
1771
1878
|
reqOptions.headers = mergedHeaders;
|
|
1772
1879
|
}
|
|
1880
|
+
if (fetchMode) {
|
|
1881
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1882
|
+
}
|
|
1883
|
+
if (timeout) {
|
|
1884
|
+
// Public API: seconds. clib post_async path enforces in seconds.
|
|
1885
|
+
reqOptions.timeout = timeout;
|
|
1886
|
+
}
|
|
1773
1887
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1774
1888
|
|
|
1775
1889
|
// Register async request with callback manager
|
|
@@ -1791,7 +1905,7 @@ class Session {
|
|
|
1791
1905
|
* @returns {Promise<Response>} Response object
|
|
1792
1906
|
*/
|
|
1793
1907
|
request(method, url, options = {}) {
|
|
1794
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
|
|
1908
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
1795
1909
|
|
|
1796
1910
|
url = addParamsToUrl(url, params);
|
|
1797
1911
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1837,6 +1951,7 @@ class Session {
|
|
|
1837
1951
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1838
1952
|
if (body) requestConfig.body = body;
|
|
1839
1953
|
if (timeout) requestConfig.timeout = timeout;
|
|
1954
|
+
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1840
1955
|
|
|
1841
1956
|
// Register async request with callback manager
|
|
1842
1957
|
const manager = getAsyncManager();
|
|
@@ -1902,25 +2017,15 @@ class Session {
|
|
|
1902
2017
|
}
|
|
1903
2018
|
|
|
1904
2019
|
/**
|
|
1905
|
-
* Get all cookies
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
2020
|
+
* Get all cookies from the session with full metadata.
|
|
2021
|
+
*
|
|
2022
|
+
* Returns Cookie[] with domain/path/expiry/flags. For the older flat
|
|
2023
|
+
* name->value object, build it yourself:
|
|
2024
|
+
* `Object.fromEntries(session.getCookies().map(c => [c.name, c.value]))`.
|
|
2025
|
+
* @returns {Cookie[]}
|
|
1909
2026
|
*/
|
|
1910
2027
|
getCookies() {
|
|
1911
|
-
|
|
1912
|
-
Session._getCookiesDeprecated = true;
|
|
1913
|
-
process.emitWarning(
|
|
1914
|
-
'getCookies() currently returns a flat {name: value} object. In a future release, it will return Cookie[] with full metadata (domain, path, expiry, etc.), same as getCookiesDetailed(). Update your code accordingly.',
|
|
1915
|
-
'DeprecationWarning'
|
|
1916
|
-
);
|
|
1917
|
-
}
|
|
1918
|
-
const cookies = this.getCookiesDetailed();
|
|
1919
|
-
const result = {};
|
|
1920
|
-
for (const c of cookies) {
|
|
1921
|
-
result[c.name] = c.value;
|
|
1922
|
-
}
|
|
1923
|
-
return result;
|
|
2028
|
+
return this.getCookiesDetailed();
|
|
1924
2029
|
}
|
|
1925
2030
|
|
|
1926
2031
|
/**
|
|
@@ -1934,22 +2039,15 @@ class Session {
|
|
|
1934
2039
|
}
|
|
1935
2040
|
|
|
1936
2041
|
/**
|
|
1937
|
-
* Get a specific cookie
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
2042
|
+
* Get a specific cookie by name.
|
|
2043
|
+
*
|
|
2044
|
+
* Returns a Cookie object (with domain/path/expiry/flags) or null. For just
|
|
2045
|
+
* the string value: `const c = session.getCookie('foo'); const v = c?.value`.
|
|
1940
2046
|
* @param {string} name - Cookie name
|
|
1941
|
-
* @returns {
|
|
2047
|
+
* @returns {Cookie|null}
|
|
1942
2048
|
*/
|
|
1943
2049
|
getCookie(name) {
|
|
1944
|
-
|
|
1945
|
-
Session._getCookieDeprecated = true;
|
|
1946
|
-
process.emitWarning(
|
|
1947
|
-
'getCookie() currently returns a string value. In a future release, it will return a Cookie object with full metadata (domain, path, expiry, etc.), same as getCookieDetailed(). Update your code accordingly.',
|
|
1948
|
-
'DeprecationWarning'
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
const cookie = this.getCookieDetailed(name);
|
|
1952
|
-
return cookie ? cookie.value : null;
|
|
2050
|
+
return this.getCookieDetailed(name);
|
|
1953
2051
|
}
|
|
1954
2052
|
|
|
1955
2053
|
/**
|
|
@@ -2817,8 +2915,8 @@ let _defaultConfig = {};
|
|
|
2817
2915
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
2818
2916
|
* @param {boolean} [options.allowRedirects=true] - Follow redirects
|
|
2819
2917
|
* @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
|
|
2820
|
-
* @param {number} [options.retry=
|
|
2821
|
-
* @param {number[]} [options.retryOnStatus] - Status codes to retry on
|
|
2918
|
+
* @param {number} [options.retry=0] - Number of retries on failure (default 0 = no retries; set positive to enable. See issue #57)
|
|
2919
|
+
* @param {number[]} [options.retryOnStatus] - Status codes to retry on (only consulted when retry > 0)
|
|
2822
2920
|
*/
|
|
2823
2921
|
function configure(options = {}) {
|
|
2824
2922
|
const {
|
|
@@ -2831,7 +2929,7 @@ function configure(options = {}) {
|
|
|
2831
2929
|
verify = true,
|
|
2832
2930
|
allowRedirects = true,
|
|
2833
2931
|
maxRedirects = 10,
|
|
2834
|
-
retry =
|
|
2932
|
+
retry = 0,
|
|
2835
2933
|
retryOnStatus = null,
|
|
2836
2934
|
} = options;
|
|
2837
2935
|
|
|
@@ -2887,7 +2985,7 @@ function _getDefaultSession() {
|
|
|
2887
2985
|
const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
|
|
2888
2986
|
const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
|
|
2889
2987
|
const maxRedirects = _defaultConfig.maxRedirects || 10;
|
|
2890
|
-
const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry :
|
|
2988
|
+
const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 0;
|
|
2891
2989
|
const retryOnStatus = _defaultConfig.retryOnStatus || null;
|
|
2892
2990
|
const headers = _defaultConfig.headers || {};
|
|
2893
2991
|
|
|
@@ -3534,10 +3632,247 @@ function clearSessionCache() {
|
|
|
3534
3632
|
}
|
|
3535
3633
|
|
|
3536
3634
|
|
|
3635
|
+
// --- String Memory Management for Preset/Pool Functions ---
|
|
3636
|
+
|
|
3637
|
+
/**
|
|
3638
|
+
* Read a C string from a void pointer and free the native memory.
|
|
3639
|
+
* Returns null if the pointer is null/0.
|
|
3640
|
+
* @param {*} ptr - Native void pointer
|
|
3641
|
+
* @returns {string|null}
|
|
3642
|
+
*/
|
|
3643
|
+
function readAndFreeString(ptr) {
|
|
3644
|
+
if (!ptr) return null;
|
|
3645
|
+
const lib = getLib();
|
|
3646
|
+
try {
|
|
3647
|
+
return koffi.decode(ptr, "char", -1);
|
|
3648
|
+
} finally {
|
|
3649
|
+
lib.httpcloak_free_string(ptr);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// --- Custom Preset Loading ---
|
|
3654
|
+
|
|
3655
|
+
/**
|
|
3656
|
+
* Load a custom preset from a JSON file and register it.
|
|
3657
|
+
* @param {string} filePath - Path to the preset JSON file
|
|
3658
|
+
* @returns {string} The registered preset name
|
|
3659
|
+
*/
|
|
3660
|
+
function loadPreset(filePath) {
|
|
3661
|
+
const lib = getLib();
|
|
3662
|
+
const result = readAndFreeString(lib.httpcloak_preset_load_file(filePath));
|
|
3663
|
+
if (!result) {
|
|
3664
|
+
throw new HTTPCloakError("Failed to load preset from file");
|
|
3665
|
+
}
|
|
3666
|
+
const data = JSON.parse(result);
|
|
3667
|
+
if (data.error) {
|
|
3668
|
+
throw new HTTPCloakError(data.error);
|
|
3669
|
+
}
|
|
3670
|
+
return data.name;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
/**
|
|
3674
|
+
* Load a custom preset from a JSON string and register it.
|
|
3675
|
+
* @param {string} jsonData - JSON string defining the preset
|
|
3676
|
+
* @returns {string} The registered preset name
|
|
3677
|
+
*/
|
|
3678
|
+
function loadPresetFromJSON(jsonData) {
|
|
3679
|
+
const lib = getLib();
|
|
3680
|
+
const result = readAndFreeString(lib.httpcloak_preset_load_json(jsonData));
|
|
3681
|
+
if (!result) {
|
|
3682
|
+
throw new HTTPCloakError("Failed to load preset from JSON");
|
|
3683
|
+
}
|
|
3684
|
+
const data = JSON.parse(result);
|
|
3685
|
+
if (data.error) {
|
|
3686
|
+
throw new HTTPCloakError(data.error);
|
|
3687
|
+
}
|
|
3688
|
+
return data.name;
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
/**
|
|
3692
|
+
* Unregister a custom preset by name.
|
|
3693
|
+
* @param {string} name - The preset name to unregister
|
|
3694
|
+
*/
|
|
3695
|
+
function unregisterPreset(name) {
|
|
3696
|
+
const lib = getLib();
|
|
3697
|
+
lib.httpcloak_preset_unregister(name);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
/**
|
|
3701
|
+
* Return a fully-resolved JSON document for the given preset.
|
|
3702
|
+
*
|
|
3703
|
+
* The output flattens any inheritance chain and resolves H2/H3 defaults
|
|
3704
|
+
* (Chrome unless the preset overrides) into explicit values, so the
|
|
3705
|
+
* result is suitable for saving, editing, and reloading via
|
|
3706
|
+
* loadPresetFromJSON. Two consecutive calls return byte-identical JSON.
|
|
3707
|
+
*
|
|
3708
|
+
* @param {string} name - The preset name (built-in or custom-registered).
|
|
3709
|
+
* @returns {string} JSON string of the form {"version":1,"preset":{...}}.
|
|
3710
|
+
* @throws {HTTPCloakError} If the preset is not registered or references
|
|
3711
|
+
* an unknown utls ClientHelloID.
|
|
3712
|
+
*/
|
|
3713
|
+
function describePreset(name) {
|
|
3714
|
+
const lib = getLib();
|
|
3715
|
+
const result = lib.httpcloak_describe_preset(name);
|
|
3716
|
+
if (!result) {
|
|
3717
|
+
throw new HTTPCloakError(`Failed to describe preset: ${name}`);
|
|
3718
|
+
}
|
|
3719
|
+
// The error envelope ({"error": "..."}) is also a valid JSON document.
|
|
3720
|
+
// A successful describe always carries a top-level "preset" field, so
|
|
3721
|
+
// distinguishing the two is straightforward.
|
|
3722
|
+
let parsed;
|
|
3723
|
+
try {
|
|
3724
|
+
parsed = JSON.parse(result);
|
|
3725
|
+
} catch (err) {
|
|
3726
|
+
throw new HTTPCloakError(`Invalid describe_preset response: ${err.message}`);
|
|
3727
|
+
}
|
|
3728
|
+
if (parsed && parsed.error && !parsed.preset) {
|
|
3729
|
+
throw new HTTPCloakError(parsed.error);
|
|
3730
|
+
}
|
|
3731
|
+
return result;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
/**
|
|
3735
|
+
* A pool of custom fingerprint presets for rotation.
|
|
3736
|
+
*
|
|
3737
|
+
* Pools load multiple presets from a single JSON file and provide
|
|
3738
|
+
* round-robin or random selection. All presets are auto-registered
|
|
3739
|
+
* on construction, so you can pass the returned name directly to
|
|
3740
|
+
* Session({ preset: name }).
|
|
3741
|
+
*/
|
|
3742
|
+
class PresetPool {
|
|
3743
|
+
/**
|
|
3744
|
+
* Load a preset pool from a JSON file.
|
|
3745
|
+
* @param {string} filePath - Path to the pool JSON file
|
|
3746
|
+
*/
|
|
3747
|
+
constructor(filePath) {
|
|
3748
|
+
this._lib = getLib();
|
|
3749
|
+
const result = readAndFreeString(this._lib.httpcloak_pool_load_file(filePath));
|
|
3750
|
+
if (!result) {
|
|
3751
|
+
throw new HTTPCloakError(`Failed to load preset pool from file: ${filePath}`);
|
|
3752
|
+
}
|
|
3753
|
+
const data = JSON.parse(result);
|
|
3754
|
+
if (data.error) {
|
|
3755
|
+
throw new HTTPCloakError(data.error);
|
|
3756
|
+
}
|
|
3757
|
+
this._handle = BigInt(data.handle);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
/**
|
|
3761
|
+
* Load a preset pool from a JSON string.
|
|
3762
|
+
* @param {string} jsonData - JSON string defining the pool
|
|
3763
|
+
* @returns {PresetPool}
|
|
3764
|
+
*/
|
|
3765
|
+
static fromJSON(jsonData) {
|
|
3766
|
+
const pool = Object.create(PresetPool.prototype);
|
|
3767
|
+
pool._lib = getLib();
|
|
3768
|
+
const result = readAndFreeString(pool._lib.httpcloak_pool_load_json(jsonData));
|
|
3769
|
+
if (!result) {
|
|
3770
|
+
throw new HTTPCloakError("Failed to load preset pool from JSON");
|
|
3771
|
+
}
|
|
3772
|
+
const data = JSON.parse(result);
|
|
3773
|
+
if (data.error) {
|
|
3774
|
+
throw new HTTPCloakError(data.error);
|
|
3775
|
+
}
|
|
3776
|
+
pool._handle = BigInt(data.handle);
|
|
3777
|
+
return pool;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
/**
|
|
3781
|
+
* Pick a preset using the pool's configured strategy.
|
|
3782
|
+
* @returns {string} Preset name
|
|
3783
|
+
*/
|
|
3784
|
+
pick() {
|
|
3785
|
+
this._checkHandle();
|
|
3786
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_pick(this._handle));
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
/**
|
|
3790
|
+
* Pick a random preset from the pool.
|
|
3791
|
+
* @returns {string} Preset name
|
|
3792
|
+
*/
|
|
3793
|
+
random() {
|
|
3794
|
+
this._checkHandle();
|
|
3795
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_random(this._handle));
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
/**
|
|
3799
|
+
* Pick the next preset in round-robin order.
|
|
3800
|
+
* @returns {string} Preset name
|
|
3801
|
+
*/
|
|
3802
|
+
next() {
|
|
3803
|
+
this._checkHandle();
|
|
3804
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_next(this._handle));
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
/**
|
|
3808
|
+
* Get a preset by index.
|
|
3809
|
+
* @param {number} index - Zero-based index
|
|
3810
|
+
* @returns {string} Preset name
|
|
3811
|
+
*/
|
|
3812
|
+
get(index) {
|
|
3813
|
+
this._checkHandle();
|
|
3814
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_get(this._handle, index));
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
/**
|
|
3818
|
+
* Number of presets in the pool.
|
|
3819
|
+
* @type {number}
|
|
3820
|
+
*/
|
|
3821
|
+
get size() {
|
|
3822
|
+
this._checkHandle();
|
|
3823
|
+
const s = this._lib.httpcloak_pool_size(this._handle);
|
|
3824
|
+
if (s < 0) {
|
|
3825
|
+
throw new HTTPCloakError("Failed to get pool size");
|
|
3826
|
+
}
|
|
3827
|
+
return Number(s);
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
/**
|
|
3831
|
+
* Name of the preset pool.
|
|
3832
|
+
* @type {string}
|
|
3833
|
+
*/
|
|
3834
|
+
get name() {
|
|
3835
|
+
this._checkHandle();
|
|
3836
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_name(this._handle));
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
/**
|
|
3840
|
+
* Free the pool handle and unregister all its presets.
|
|
3841
|
+
*/
|
|
3842
|
+
close() {
|
|
3843
|
+
if (this._handle && this._handle !== 0n && this._handle !== 0) {
|
|
3844
|
+
this._lib.httpcloak_pool_free(this._handle);
|
|
3845
|
+
this._handle = 0n;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
_parsePoolResult(resultPtr) {
|
|
3850
|
+
const result = readAndFreeString(resultPtr);
|
|
3851
|
+
if (!result) {
|
|
3852
|
+
throw new HTTPCloakError("No result from preset pool");
|
|
3853
|
+
}
|
|
3854
|
+
// Error responses are JSON with {"error":"..."}
|
|
3855
|
+
if (result.startsWith("{")) {
|
|
3856
|
+
const data = JSON.parse(result);
|
|
3857
|
+
if (data.error) {
|
|
3858
|
+
throw new HTTPCloakError(data.error);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return result;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
_checkHandle() {
|
|
3865
|
+
if (!this._handle || this._handle === 0n || this._handle === 0) {
|
|
3866
|
+
throw new HTTPCloakError("PresetPool is closed");
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3537
3871
|
module.exports = {
|
|
3538
3872
|
// Classes
|
|
3539
3873
|
Session,
|
|
3540
3874
|
LocalProxy,
|
|
3875
|
+
PresetPool,
|
|
3541
3876
|
Response,
|
|
3542
3877
|
FastResponse,
|
|
3543
3878
|
StreamResponse,
|
|
@@ -3547,6 +3882,11 @@ module.exports = {
|
|
|
3547
3882
|
SessionCacheBackend,
|
|
3548
3883
|
// Presets
|
|
3549
3884
|
Preset,
|
|
3885
|
+
// Custom preset loading
|
|
3886
|
+
loadPreset,
|
|
3887
|
+
loadPresetFromJSON,
|
|
3888
|
+
unregisterPreset,
|
|
3889
|
+
describePreset,
|
|
3550
3890
|
// Configuration
|
|
3551
3891
|
configure,
|
|
3552
3892
|
configureSessionCache,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "httpcloak",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
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.1"
|
|
52
|
+
"@httpcloak/darwin-arm64": "1.6.6",
|
|
53
|
+
"@httpcloak/darwin-x64": "1.6.6",
|
|
54
|
+
"@httpcloak/linux-arm64": "1.6.6",
|
|
55
|
+
"@httpcloak/linux-x64": "1.6.6",
|
|
56
|
+
"@httpcloak/win32-x64": "1.6.6"
|
|
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.1",
|
|
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
|
-
}
|