httpcloak 1.6.1 → 1.6.5
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 +76 -0
- package/lib/index.js +447 -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-arm64/package.json +1 -1
- package/npm/win32-x64/package.json +1 -1
- package/package.json +7 -7
package/lib/index.d.ts
CHANGED
|
@@ -268,6 +268,19 @@ export interface RequestOptions {
|
|
|
268
268
|
auth?: [string, string];
|
|
269
269
|
/** Optional request timeout in seconds */
|
|
270
270
|
timeout?: number;
|
|
271
|
+
/**
|
|
272
|
+
* Explicit Sec-Fetch-Mode/Dest override for requests where auto-sniffing isn't enough.
|
|
273
|
+
*
|
|
274
|
+
* Valid values:
|
|
275
|
+
* - `"cors"` - XHR/fetch() request (Sec-Fetch-Mode: cors, Sec-Fetch-Dest: empty, Sec-Fetch-Site: same-origin)
|
|
276
|
+
* - `"no-cors"` - Subresource load (image/script/stylesheet tag)
|
|
277
|
+
* - `"navigate"` - Top-level navigation (document load, classic form POST)
|
|
278
|
+
* - `"websocket"` - WebSocket upgrade
|
|
279
|
+
*
|
|
280
|
+
* When unset (default), httpcloak auto-detects based on method, Accept, Content-Type, and Sec-Fetch-Dest headers.
|
|
281
|
+
* Set this explicitly when the auto-sniff gets it wrong (e.g., POST to a CORS endpoint without a JSON Accept header).
|
|
282
|
+
*/
|
|
283
|
+
fetchMode?: "cors" | "no-cors" | "navigate" | "websocket";
|
|
271
284
|
}
|
|
272
285
|
|
|
273
286
|
export class Session {
|
|
@@ -1020,3 +1033,66 @@ export function configureSessionCache(options: SessionCacheOptions): SessionCach
|
|
|
1020
1033
|
* After calling this, new sessions will not use distributed caching.
|
|
1021
1034
|
*/
|
|
1022
1035
|
export function clearSessionCache(): void;
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Load a custom preset from a JSON file and register it.
|
|
1039
|
+
* @param filePath - Path to the preset JSON file
|
|
1040
|
+
* @returns The registered preset name
|
|
1041
|
+
*/
|
|
1042
|
+
export function loadPreset(filePath: string): string;
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Load a custom preset from a JSON string and register it.
|
|
1046
|
+
* @param jsonData - JSON string defining the preset
|
|
1047
|
+
* @returns The registered preset name
|
|
1048
|
+
*/
|
|
1049
|
+
export function loadPresetFromJSON(jsonData: string): string;
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Unregister a custom preset by name.
|
|
1053
|
+
* @param name - The preset name to unregister
|
|
1054
|
+
*/
|
|
1055
|
+
export function unregisterPreset(name: string): void;
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* A pool of custom fingerprint presets for rotation.
|
|
1059
|
+
*
|
|
1060
|
+
* Pools load multiple presets from a single JSON file and provide
|
|
1061
|
+
* round-robin or random selection. All presets are auto-registered
|
|
1062
|
+
* on construction, so you can pass the returned name directly to
|
|
1063
|
+
* `new Session({ preset: name })`.
|
|
1064
|
+
*/
|
|
1065
|
+
export class PresetPool {
|
|
1066
|
+
/**
|
|
1067
|
+
* Load a preset pool from a JSON file.
|
|
1068
|
+
* @param filePath - Path to the pool JSON file
|
|
1069
|
+
*/
|
|
1070
|
+
constructor(filePath: string);
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Load a preset pool from a JSON string.
|
|
1074
|
+
* @param jsonData - JSON string defining the pool
|
|
1075
|
+
*/
|
|
1076
|
+
static fromJSON(jsonData: string): PresetPool;
|
|
1077
|
+
|
|
1078
|
+
/** Pick a preset using the pool's configured strategy. */
|
|
1079
|
+
pick(): string;
|
|
1080
|
+
|
|
1081
|
+
/** Pick a random preset from the pool. */
|
|
1082
|
+
random(): string;
|
|
1083
|
+
|
|
1084
|
+
/** Pick the next preset in round-robin order. */
|
|
1085
|
+
next(): string;
|
|
1086
|
+
|
|
1087
|
+
/** Get a preset by index. */
|
|
1088
|
+
get(index: number): string;
|
|
1089
|
+
|
|
1090
|
+
/** Number of presets in the pool. */
|
|
1091
|
+
readonly size: number;
|
|
1092
|
+
|
|
1093
|
+
/** Name of the preset pool. */
|
|
1094
|
+
readonly name: string;
|
|
1095
|
+
|
|
1096
|
+
/** Free the pool handle and unregister all its presets. */
|
|
1097
|
+
close(): void;
|
|
1098
|
+
}
|
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,
|
|
@@ -1522,7 +1598,7 @@ class Session {
|
|
|
1522
1598
|
* @returns {Response} Response object
|
|
1523
1599
|
*/
|
|
1524
1600
|
getSync(url, options = {}) {
|
|
1525
|
-
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1601
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1526
1602
|
|
|
1527
1603
|
url = addParamsToUrl(url, params);
|
|
1528
1604
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1536,12 +1612,18 @@ class Session {
|
|
|
1536
1612
|
if (mergedHeaders) {
|
|
1537
1613
|
reqOptions.headers = mergedHeaders;
|
|
1538
1614
|
}
|
|
1615
|
+
if (fetchMode) {
|
|
1616
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1617
|
+
}
|
|
1539
1618
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1540
1619
|
|
|
1541
1620
|
const startTime = Date.now();
|
|
1542
|
-
const
|
|
1621
|
+
const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
|
|
1543
1622
|
const elapsed = Date.now() - startTime;
|
|
1544
|
-
|
|
1623
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1624
|
+
throw new HTTPCloakError("Request failed");
|
|
1625
|
+
}
|
|
1626
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1545
1627
|
}
|
|
1546
1628
|
|
|
1547
1629
|
/**
|
|
@@ -1562,38 +1644,36 @@ class Session {
|
|
|
1562
1644
|
* @returns {Response} Response object
|
|
1563
1645
|
*/
|
|
1564
1646
|
postSync(url, options = {}) {
|
|
1565
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
1647
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1566
1648
|
|
|
1567
1649
|
url = addParamsToUrl(url, params);
|
|
1568
1650
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1569
1651
|
|
|
1570
|
-
//
|
|
1652
|
+
// Normalize body to a Buffer so the raw path can pass (ptr, len) without
|
|
1653
|
+
// null-terminator truncation or utf8 mangling.
|
|
1654
|
+
let bodyBuffer = null;
|
|
1571
1655
|
if (files !== null) {
|
|
1572
1656
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1573
1657
|
const multipart = encodeMultipart(formData, files);
|
|
1574
|
-
|
|
1658
|
+
bodyBuffer = multipart.body;
|
|
1575
1659
|
mergedHeaders = mergedHeaders || {};
|
|
1576
1660
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
else if (json !== null) {
|
|
1580
|
-
body = JSON.stringify(json);
|
|
1661
|
+
} else if (json !== null) {
|
|
1662
|
+
bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
|
|
1581
1663
|
mergedHeaders = mergedHeaders || {};
|
|
1582
1664
|
if (!mergedHeaders["Content-Type"]) {
|
|
1583
1665
|
mergedHeaders["Content-Type"] = "application/json";
|
|
1584
1666
|
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
else if (data !== null && typeof data === "object") {
|
|
1588
|
-
body = new URLSearchParams(data).toString();
|
|
1667
|
+
} else if (data !== null && typeof data === "object") {
|
|
1668
|
+
bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
|
|
1589
1669
|
mergedHeaders = mergedHeaders || {};
|
|
1590
1670
|
if (!mergedHeaders["Content-Type"]) {
|
|
1591
1671
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1592
1672
|
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
else if (
|
|
1596
|
-
|
|
1673
|
+
} else if (Buffer.isBuffer(body)) {
|
|
1674
|
+
bodyBuffer = body;
|
|
1675
|
+
} else if (typeof body === "string") {
|
|
1676
|
+
bodyBuffer = Buffer.from(body, "utf8");
|
|
1597
1677
|
}
|
|
1598
1678
|
|
|
1599
1679
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1606,12 +1686,21 @@ class Session {
|
|
|
1606
1686
|
if (mergedHeaders) {
|
|
1607
1687
|
reqOptions.headers = mergedHeaders;
|
|
1608
1688
|
}
|
|
1689
|
+
if (fetchMode) {
|
|
1690
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1691
|
+
}
|
|
1609
1692
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1610
1693
|
|
|
1694
|
+
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1695
|
+
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
1696
|
+
|
|
1611
1697
|
const startTime = Date.now();
|
|
1612
|
-
const
|
|
1698
|
+
const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, bodyPtr, bodyLen, optionsJson);
|
|
1613
1699
|
const elapsed = Date.now() - startTime;
|
|
1614
|
-
|
|
1700
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1701
|
+
throw new HTTPCloakError("Request failed");
|
|
1702
|
+
}
|
|
1703
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1615
1704
|
}
|
|
1616
1705
|
|
|
1617
1706
|
/**
|
|
@@ -1624,38 +1713,35 @@ class Session {
|
|
|
1624
1713
|
* @returns {Response} Response object
|
|
1625
1714
|
*/
|
|
1626
1715
|
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;
|
|
1716
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
1628
1717
|
|
|
1629
1718
|
url = addParamsToUrl(url, params);
|
|
1630
1719
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1631
1720
|
|
|
1632
|
-
//
|
|
1721
|
+
// Normalize body to a Buffer so the raw path can pass (ptr, len).
|
|
1722
|
+
let bodyBuffer = null;
|
|
1633
1723
|
if (files !== null) {
|
|
1634
1724
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1635
1725
|
const multipart = encodeMultipart(formData, files);
|
|
1636
|
-
|
|
1726
|
+
bodyBuffer = multipart.body;
|
|
1637
1727
|
mergedHeaders = mergedHeaders || {};
|
|
1638
1728
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
else if (json !== null) {
|
|
1642
|
-
body = JSON.stringify(json);
|
|
1729
|
+
} else if (json !== null) {
|
|
1730
|
+
bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
|
|
1643
1731
|
mergedHeaders = mergedHeaders || {};
|
|
1644
1732
|
if (!mergedHeaders["Content-Type"]) {
|
|
1645
1733
|
mergedHeaders["Content-Type"] = "application/json";
|
|
1646
1734
|
}
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
else if (data !== null && typeof data === "object") {
|
|
1650
|
-
body = new URLSearchParams(data).toString();
|
|
1735
|
+
} else if (data !== null && typeof data === "object") {
|
|
1736
|
+
bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
|
|
1651
1737
|
mergedHeaders = mergedHeaders || {};
|
|
1652
1738
|
if (!mergedHeaders["Content-Type"]) {
|
|
1653
1739
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1654
1740
|
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
else if (
|
|
1658
|
-
|
|
1741
|
+
} else if (Buffer.isBuffer(body)) {
|
|
1742
|
+
bodyBuffer = body;
|
|
1743
|
+
} else if (typeof body === "string") {
|
|
1744
|
+
bodyBuffer = Buffer.from(body, "utf8");
|
|
1659
1745
|
}
|
|
1660
1746
|
|
|
1661
1747
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1668,16 +1754,24 @@ class Session {
|
|
|
1668
1754
|
url,
|
|
1669
1755
|
};
|
|
1670
1756
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1671
|
-
if (body) requestConfig.body = body;
|
|
1672
1757
|
if (timeout) requestConfig.timeout = timeout;
|
|
1758
|
+
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1759
|
+
|
|
1760
|
+
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1761
|
+
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
1673
1762
|
|
|
1674
1763
|
const startTime = Date.now();
|
|
1675
|
-
const
|
|
1764
|
+
const responseHandle = this._lib.httpcloak_request_raw(
|
|
1676
1765
|
this._handle,
|
|
1677
|
-
JSON.stringify(requestConfig)
|
|
1766
|
+
JSON.stringify(requestConfig),
|
|
1767
|
+
bodyPtr,
|
|
1768
|
+
bodyLen
|
|
1678
1769
|
);
|
|
1679
1770
|
const elapsed = Date.now() - startTime;
|
|
1680
|
-
|
|
1771
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1772
|
+
throw new HTTPCloakError("Request failed");
|
|
1773
|
+
}
|
|
1774
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1681
1775
|
}
|
|
1682
1776
|
|
|
1683
1777
|
// ===========================================================================
|
|
@@ -1692,7 +1786,7 @@ class Session {
|
|
|
1692
1786
|
* @returns {Promise<Response>} Response object
|
|
1693
1787
|
*/
|
|
1694
1788
|
get(url, options = {}) {
|
|
1695
|
-
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1789
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1696
1790
|
|
|
1697
1791
|
url = addParamsToUrl(url, params);
|
|
1698
1792
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1706,6 +1800,15 @@ class Session {
|
|
|
1706
1800
|
if (mergedHeaders) {
|
|
1707
1801
|
reqOptions.headers = mergedHeaders;
|
|
1708
1802
|
}
|
|
1803
|
+
if (fetchMode) {
|
|
1804
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1805
|
+
}
|
|
1806
|
+
if (timeout) {
|
|
1807
|
+
// Public API: seconds (matches Session({ timeout }) and the
|
|
1808
|
+
// existing request()/put()/delete()/etc. methods which forward to
|
|
1809
|
+
// httpcloak_request_async — that path also uses time.Second).
|
|
1810
|
+
reqOptions.timeout = timeout;
|
|
1811
|
+
}
|
|
1709
1812
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1710
1813
|
|
|
1711
1814
|
// Register async request with callback manager
|
|
@@ -1726,7 +1829,7 @@ class Session {
|
|
|
1726
1829
|
* @returns {Promise<Response>} Response object
|
|
1727
1830
|
*/
|
|
1728
1831
|
post(url, options = {}) {
|
|
1729
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
1832
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1730
1833
|
|
|
1731
1834
|
url = addParamsToUrl(url, params);
|
|
1732
1835
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1770,6 +1873,13 @@ class Session {
|
|
|
1770
1873
|
if (mergedHeaders) {
|
|
1771
1874
|
reqOptions.headers = mergedHeaders;
|
|
1772
1875
|
}
|
|
1876
|
+
if (fetchMode) {
|
|
1877
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1878
|
+
}
|
|
1879
|
+
if (timeout) {
|
|
1880
|
+
// Public API: seconds. clib post_async path enforces in seconds.
|
|
1881
|
+
reqOptions.timeout = timeout;
|
|
1882
|
+
}
|
|
1773
1883
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1774
1884
|
|
|
1775
1885
|
// Register async request with callback manager
|
|
@@ -1791,7 +1901,7 @@ class Session {
|
|
|
1791
1901
|
* @returns {Promise<Response>} Response object
|
|
1792
1902
|
*/
|
|
1793
1903
|
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;
|
|
1904
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
1795
1905
|
|
|
1796
1906
|
url = addParamsToUrl(url, params);
|
|
1797
1907
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1837,6 +1947,7 @@ class Session {
|
|
|
1837
1947
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1838
1948
|
if (body) requestConfig.body = body;
|
|
1839
1949
|
if (timeout) requestConfig.timeout = timeout;
|
|
1950
|
+
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1840
1951
|
|
|
1841
1952
|
// Register async request with callback manager
|
|
1842
1953
|
const manager = getAsyncManager();
|
|
@@ -1902,25 +2013,15 @@ class Session {
|
|
|
1902
2013
|
}
|
|
1903
2014
|
|
|
1904
2015
|
/**
|
|
1905
|
-
* Get all cookies
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
2016
|
+
* Get all cookies from the session with full metadata.
|
|
2017
|
+
*
|
|
2018
|
+
* Returns Cookie[] with domain/path/expiry/flags. For the older flat
|
|
2019
|
+
* name->value object, build it yourself:
|
|
2020
|
+
* `Object.fromEntries(session.getCookies().map(c => [c.name, c.value]))`.
|
|
2021
|
+
* @returns {Cookie[]}
|
|
1909
2022
|
*/
|
|
1910
2023
|
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;
|
|
2024
|
+
return this.getCookiesDetailed();
|
|
1924
2025
|
}
|
|
1925
2026
|
|
|
1926
2027
|
/**
|
|
@@ -1934,22 +2035,15 @@ class Session {
|
|
|
1934
2035
|
}
|
|
1935
2036
|
|
|
1936
2037
|
/**
|
|
1937
|
-
* Get a specific cookie
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
2038
|
+
* Get a specific cookie by name.
|
|
2039
|
+
*
|
|
2040
|
+
* Returns a Cookie object (with domain/path/expiry/flags) or null. For just
|
|
2041
|
+
* the string value: `const c = session.getCookie('foo'); const v = c?.value`.
|
|
1940
2042
|
* @param {string} name - Cookie name
|
|
1941
|
-
* @returns {
|
|
2043
|
+
* @returns {Cookie|null}
|
|
1942
2044
|
*/
|
|
1943
2045
|
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;
|
|
2046
|
+
return this.getCookieDetailed(name);
|
|
1953
2047
|
}
|
|
1954
2048
|
|
|
1955
2049
|
/**
|
|
@@ -2817,8 +2911,8 @@ let _defaultConfig = {};
|
|
|
2817
2911
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
2818
2912
|
* @param {boolean} [options.allowRedirects=true] - Follow redirects
|
|
2819
2913
|
* @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
|
|
2914
|
+
* @param {number} [options.retry=0] - Number of retries on failure (default 0 = no retries; set positive to enable. See issue #57)
|
|
2915
|
+
* @param {number[]} [options.retryOnStatus] - Status codes to retry on (only consulted when retry > 0)
|
|
2822
2916
|
*/
|
|
2823
2917
|
function configure(options = {}) {
|
|
2824
2918
|
const {
|
|
@@ -2831,7 +2925,7 @@ function configure(options = {}) {
|
|
|
2831
2925
|
verify = true,
|
|
2832
2926
|
allowRedirects = true,
|
|
2833
2927
|
maxRedirects = 10,
|
|
2834
|
-
retry =
|
|
2928
|
+
retry = 0,
|
|
2835
2929
|
retryOnStatus = null,
|
|
2836
2930
|
} = options;
|
|
2837
2931
|
|
|
@@ -2887,7 +2981,7 @@ function _getDefaultSession() {
|
|
|
2887
2981
|
const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
|
|
2888
2982
|
const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
|
|
2889
2983
|
const maxRedirects = _defaultConfig.maxRedirects || 10;
|
|
2890
|
-
const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry :
|
|
2984
|
+
const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 0;
|
|
2891
2985
|
const retryOnStatus = _defaultConfig.retryOnStatus || null;
|
|
2892
2986
|
const headers = _defaultConfig.headers || {};
|
|
2893
2987
|
|
|
@@ -3534,10 +3628,247 @@ function clearSessionCache() {
|
|
|
3534
3628
|
}
|
|
3535
3629
|
|
|
3536
3630
|
|
|
3631
|
+
// --- String Memory Management for Preset/Pool Functions ---
|
|
3632
|
+
|
|
3633
|
+
/**
|
|
3634
|
+
* Read a C string from a void pointer and free the native memory.
|
|
3635
|
+
* Returns null if the pointer is null/0.
|
|
3636
|
+
* @param {*} ptr - Native void pointer
|
|
3637
|
+
* @returns {string|null}
|
|
3638
|
+
*/
|
|
3639
|
+
function readAndFreeString(ptr) {
|
|
3640
|
+
if (!ptr) return null;
|
|
3641
|
+
const lib = getLib();
|
|
3642
|
+
try {
|
|
3643
|
+
return koffi.decode(ptr, "char", -1);
|
|
3644
|
+
} finally {
|
|
3645
|
+
lib.httpcloak_free_string(ptr);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
// --- Custom Preset Loading ---
|
|
3650
|
+
|
|
3651
|
+
/**
|
|
3652
|
+
* Load a custom preset from a JSON file and register it.
|
|
3653
|
+
* @param {string} filePath - Path to the preset JSON file
|
|
3654
|
+
* @returns {string} The registered preset name
|
|
3655
|
+
*/
|
|
3656
|
+
function loadPreset(filePath) {
|
|
3657
|
+
const lib = getLib();
|
|
3658
|
+
const result = readAndFreeString(lib.httpcloak_preset_load_file(filePath));
|
|
3659
|
+
if (!result) {
|
|
3660
|
+
throw new HTTPCloakError("Failed to load preset from file");
|
|
3661
|
+
}
|
|
3662
|
+
const data = JSON.parse(result);
|
|
3663
|
+
if (data.error) {
|
|
3664
|
+
throw new HTTPCloakError(data.error);
|
|
3665
|
+
}
|
|
3666
|
+
return data.name;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/**
|
|
3670
|
+
* Load a custom preset from a JSON string and register it.
|
|
3671
|
+
* @param {string} jsonData - JSON string defining the preset
|
|
3672
|
+
* @returns {string} The registered preset name
|
|
3673
|
+
*/
|
|
3674
|
+
function loadPresetFromJSON(jsonData) {
|
|
3675
|
+
const lib = getLib();
|
|
3676
|
+
const result = readAndFreeString(lib.httpcloak_preset_load_json(jsonData));
|
|
3677
|
+
if (!result) {
|
|
3678
|
+
throw new HTTPCloakError("Failed to load preset from JSON");
|
|
3679
|
+
}
|
|
3680
|
+
const data = JSON.parse(result);
|
|
3681
|
+
if (data.error) {
|
|
3682
|
+
throw new HTTPCloakError(data.error);
|
|
3683
|
+
}
|
|
3684
|
+
return data.name;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
/**
|
|
3688
|
+
* Unregister a custom preset by name.
|
|
3689
|
+
* @param {string} name - The preset name to unregister
|
|
3690
|
+
*/
|
|
3691
|
+
function unregisterPreset(name) {
|
|
3692
|
+
const lib = getLib();
|
|
3693
|
+
lib.httpcloak_preset_unregister(name);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
/**
|
|
3697
|
+
* Return a fully-resolved JSON document for the given preset.
|
|
3698
|
+
*
|
|
3699
|
+
* The output flattens any inheritance chain and resolves H2/H3 defaults
|
|
3700
|
+
* (Chrome unless the preset overrides) into explicit values, so the
|
|
3701
|
+
* result is suitable for saving, editing, and reloading via
|
|
3702
|
+
* loadPresetFromJSON. Two consecutive calls return byte-identical JSON.
|
|
3703
|
+
*
|
|
3704
|
+
* @param {string} name - The preset name (built-in or custom-registered).
|
|
3705
|
+
* @returns {string} JSON string of the form {"version":1,"preset":{...}}.
|
|
3706
|
+
* @throws {HTTPCloakError} If the preset is not registered or references
|
|
3707
|
+
* an unknown utls ClientHelloID.
|
|
3708
|
+
*/
|
|
3709
|
+
function describePreset(name) {
|
|
3710
|
+
const lib = getLib();
|
|
3711
|
+
const result = lib.httpcloak_describe_preset(name);
|
|
3712
|
+
if (!result) {
|
|
3713
|
+
throw new HTTPCloakError(`Failed to describe preset: ${name}`);
|
|
3714
|
+
}
|
|
3715
|
+
// The error envelope ({"error": "..."}) is also a valid JSON document.
|
|
3716
|
+
// A successful describe always carries a top-level "preset" field, so
|
|
3717
|
+
// distinguishing the two is straightforward.
|
|
3718
|
+
let parsed;
|
|
3719
|
+
try {
|
|
3720
|
+
parsed = JSON.parse(result);
|
|
3721
|
+
} catch (err) {
|
|
3722
|
+
throw new HTTPCloakError(`Invalid describe_preset response: ${err.message}`);
|
|
3723
|
+
}
|
|
3724
|
+
if (parsed && parsed.error && !parsed.preset) {
|
|
3725
|
+
throw new HTTPCloakError(parsed.error);
|
|
3726
|
+
}
|
|
3727
|
+
return result;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
/**
|
|
3731
|
+
* A pool of custom fingerprint presets for rotation.
|
|
3732
|
+
*
|
|
3733
|
+
* Pools load multiple presets from a single JSON file and provide
|
|
3734
|
+
* round-robin or random selection. All presets are auto-registered
|
|
3735
|
+
* on construction, so you can pass the returned name directly to
|
|
3736
|
+
* Session({ preset: name }).
|
|
3737
|
+
*/
|
|
3738
|
+
class PresetPool {
|
|
3739
|
+
/**
|
|
3740
|
+
* Load a preset pool from a JSON file.
|
|
3741
|
+
* @param {string} filePath - Path to the pool JSON file
|
|
3742
|
+
*/
|
|
3743
|
+
constructor(filePath) {
|
|
3744
|
+
this._lib = getLib();
|
|
3745
|
+
const result = readAndFreeString(this._lib.httpcloak_pool_load_file(filePath));
|
|
3746
|
+
if (!result) {
|
|
3747
|
+
throw new HTTPCloakError(`Failed to load preset pool from file: ${filePath}`);
|
|
3748
|
+
}
|
|
3749
|
+
const data = JSON.parse(result);
|
|
3750
|
+
if (data.error) {
|
|
3751
|
+
throw new HTTPCloakError(data.error);
|
|
3752
|
+
}
|
|
3753
|
+
this._handle = BigInt(data.handle);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
/**
|
|
3757
|
+
* Load a preset pool from a JSON string.
|
|
3758
|
+
* @param {string} jsonData - JSON string defining the pool
|
|
3759
|
+
* @returns {PresetPool}
|
|
3760
|
+
*/
|
|
3761
|
+
static fromJSON(jsonData) {
|
|
3762
|
+
const pool = Object.create(PresetPool.prototype);
|
|
3763
|
+
pool._lib = getLib();
|
|
3764
|
+
const result = readAndFreeString(pool._lib.httpcloak_pool_load_json(jsonData));
|
|
3765
|
+
if (!result) {
|
|
3766
|
+
throw new HTTPCloakError("Failed to load preset pool from JSON");
|
|
3767
|
+
}
|
|
3768
|
+
const data = JSON.parse(result);
|
|
3769
|
+
if (data.error) {
|
|
3770
|
+
throw new HTTPCloakError(data.error);
|
|
3771
|
+
}
|
|
3772
|
+
pool._handle = BigInt(data.handle);
|
|
3773
|
+
return pool;
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
/**
|
|
3777
|
+
* Pick a preset using the pool's configured strategy.
|
|
3778
|
+
* @returns {string} Preset name
|
|
3779
|
+
*/
|
|
3780
|
+
pick() {
|
|
3781
|
+
this._checkHandle();
|
|
3782
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_pick(this._handle));
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
/**
|
|
3786
|
+
* Pick a random preset from the pool.
|
|
3787
|
+
* @returns {string} Preset name
|
|
3788
|
+
*/
|
|
3789
|
+
random() {
|
|
3790
|
+
this._checkHandle();
|
|
3791
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_random(this._handle));
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
/**
|
|
3795
|
+
* Pick the next preset in round-robin order.
|
|
3796
|
+
* @returns {string} Preset name
|
|
3797
|
+
*/
|
|
3798
|
+
next() {
|
|
3799
|
+
this._checkHandle();
|
|
3800
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_next(this._handle));
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
/**
|
|
3804
|
+
* Get a preset by index.
|
|
3805
|
+
* @param {number} index - Zero-based index
|
|
3806
|
+
* @returns {string} Preset name
|
|
3807
|
+
*/
|
|
3808
|
+
get(index) {
|
|
3809
|
+
this._checkHandle();
|
|
3810
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_get(this._handle, index));
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
/**
|
|
3814
|
+
* Number of presets in the pool.
|
|
3815
|
+
* @type {number}
|
|
3816
|
+
*/
|
|
3817
|
+
get size() {
|
|
3818
|
+
this._checkHandle();
|
|
3819
|
+
const s = this._lib.httpcloak_pool_size(this._handle);
|
|
3820
|
+
if (s < 0) {
|
|
3821
|
+
throw new HTTPCloakError("Failed to get pool size");
|
|
3822
|
+
}
|
|
3823
|
+
return Number(s);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
/**
|
|
3827
|
+
* Name of the preset pool.
|
|
3828
|
+
* @type {string}
|
|
3829
|
+
*/
|
|
3830
|
+
get name() {
|
|
3831
|
+
this._checkHandle();
|
|
3832
|
+
return this._parsePoolResult(this._lib.httpcloak_pool_name(this._handle));
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
/**
|
|
3836
|
+
* Free the pool handle and unregister all its presets.
|
|
3837
|
+
*/
|
|
3838
|
+
close() {
|
|
3839
|
+
if (this._handle && this._handle !== 0n && this._handle !== 0) {
|
|
3840
|
+
this._lib.httpcloak_pool_free(this._handle);
|
|
3841
|
+
this._handle = 0n;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
_parsePoolResult(resultPtr) {
|
|
3846
|
+
const result = readAndFreeString(resultPtr);
|
|
3847
|
+
if (!result) {
|
|
3848
|
+
throw new HTTPCloakError("No result from preset pool");
|
|
3849
|
+
}
|
|
3850
|
+
// Error responses are JSON with {"error":"..."}
|
|
3851
|
+
if (result.startsWith("{")) {
|
|
3852
|
+
const data = JSON.parse(result);
|
|
3853
|
+
if (data.error) {
|
|
3854
|
+
throw new HTTPCloakError(data.error);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
return result;
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
_checkHandle() {
|
|
3861
|
+
if (!this._handle || this._handle === 0n || this._handle === 0) {
|
|
3862
|
+
throw new HTTPCloakError("PresetPool is closed");
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3537
3867
|
module.exports = {
|
|
3538
3868
|
// Classes
|
|
3539
3869
|
Session,
|
|
3540
3870
|
LocalProxy,
|
|
3871
|
+
PresetPool,
|
|
3541
3872
|
Response,
|
|
3542
3873
|
FastResponse,
|
|
3543
3874
|
StreamResponse,
|
|
@@ -3547,6 +3878,11 @@ module.exports = {
|
|
|
3547
3878
|
SessionCacheBackend,
|
|
3548
3879
|
// Presets
|
|
3549
3880
|
Preset,
|
|
3881
|
+
// Custom preset loading
|
|
3882
|
+
loadPreset,
|
|
3883
|
+
loadPresetFromJSON,
|
|
3884
|
+
unregisterPreset,
|
|
3885
|
+
describePreset,
|
|
3550
3886
|
// Configuration
|
|
3551
3887
|
configure,
|
|
3552
3888
|
configureSessionCache,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "httpcloak",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
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,12 @@
|
|
|
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-arm64": "1.6.
|
|
57
|
-
"@httpcloak/win32-x64": "1.6.
|
|
52
|
+
"@httpcloak/darwin-arm64": "1.6.5",
|
|
53
|
+
"@httpcloak/darwin-x64": "1.6.5",
|
|
54
|
+
"@httpcloak/linux-arm64": "1.6.5",
|
|
55
|
+
"@httpcloak/linux-x64": "1.6.5",
|
|
56
|
+
"@httpcloak/win32-arm64": "1.6.5",
|
|
57
|
+
"@httpcloak/win32-x64": "1.6.5"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.1.0",
|