httpcloak 1.6.1-beta.3 → 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/README.md +47 -36
- package/lib/index.d.ts +122 -10
- package/lib/index.js +528 -115
- package/npm/darwin-arm64/package.json +1 -1
- package/npm/darwin-x64/package.json +1 -1
- package/npm/linux-arm64/package.json +1 -1
- package/npm/linux-x64/package.json +1 -1
- package/npm/win32-arm64/package.json +1 -1
- package/npm/win32-x64/package.json +1 -1
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -31,7 +31,13 @@ class HTTPCloakError extends Error {
|
|
|
31
31
|
* const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_133 });
|
|
32
32
|
*/
|
|
33
33
|
const Preset = {
|
|
34
|
-
// Chrome
|
|
34
|
+
// Chrome 146 (latest)
|
|
35
|
+
CHROME_146: "chrome-146",
|
|
36
|
+
CHROME_146_WINDOWS: "chrome-146-windows",
|
|
37
|
+
CHROME_146_LINUX: "chrome-146-linux",
|
|
38
|
+
CHROME_146_MACOS: "chrome-146-macos",
|
|
39
|
+
|
|
40
|
+
// Chrome 145
|
|
35
41
|
CHROME_145: "chrome-145",
|
|
36
42
|
CHROME_145_WINDOWS: "chrome-145-windows",
|
|
37
43
|
CHROME_145_LINUX: "chrome-145-linux",
|
|
@@ -59,9 +65,11 @@ const Preset = {
|
|
|
59
65
|
CHROME_143_IOS: "chrome-143-ios",
|
|
60
66
|
CHROME_144_IOS: "chrome-144-ios",
|
|
61
67
|
CHROME_145_IOS: "chrome-145-ios",
|
|
68
|
+
CHROME_146_IOS: "chrome-146-ios",
|
|
62
69
|
CHROME_143_ANDROID: "chrome-143-android",
|
|
63
70
|
CHROME_144_ANDROID: "chrome-144-android",
|
|
64
71
|
CHROME_145_ANDROID: "chrome-145-android",
|
|
72
|
+
CHROME_146_ANDROID: "chrome-146-android",
|
|
65
73
|
|
|
66
74
|
// Firefox
|
|
67
75
|
FIREFOX_133: "firefox-133",
|
|
@@ -75,9 +83,11 @@ const Preset = {
|
|
|
75
83
|
IOS_CHROME_143: "chrome-143-ios",
|
|
76
84
|
IOS_CHROME_144: "chrome-144-ios",
|
|
77
85
|
IOS_CHROME_145: "chrome-145-ios",
|
|
86
|
+
IOS_CHROME_146: "chrome-146-ios",
|
|
78
87
|
ANDROID_CHROME_143: "chrome-143-android",
|
|
79
88
|
ANDROID_CHROME_144: "chrome-144-android",
|
|
80
89
|
ANDROID_CHROME_145: "chrome-145-android",
|
|
90
|
+
ANDROID_CHROME_146: "chrome-146-android",
|
|
81
91
|
IOS_SAFARI_17: "safari-17-ios",
|
|
82
92
|
IOS_SAFARI_18: "safari-18-ios",
|
|
83
93
|
|
|
@@ -87,12 +97,13 @@ const Preset = {
|
|
|
87
97
|
*/
|
|
88
98
|
all() {
|
|
89
99
|
return [
|
|
100
|
+
this.CHROME_146, this.CHROME_146_WINDOWS, this.CHROME_146_LINUX, this.CHROME_146_MACOS,
|
|
90
101
|
this.CHROME_145, this.CHROME_145_WINDOWS, this.CHROME_145_LINUX, this.CHROME_145_MACOS,
|
|
91
102
|
this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
|
|
92
103
|
this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
|
|
93
104
|
this.CHROME_141, this.CHROME_133,
|
|
94
|
-
this.CHROME_145_IOS, this.CHROME_144_IOS, this.CHROME_143_IOS,
|
|
95
|
-
this.CHROME_145_ANDROID, this.CHROME_144_ANDROID, this.CHROME_143_ANDROID,
|
|
105
|
+
this.CHROME_146_IOS, this.CHROME_145_IOS, this.CHROME_144_IOS, this.CHROME_143_IOS,
|
|
106
|
+
this.CHROME_146_ANDROID, this.CHROME_145_ANDROID, this.CHROME_144_ANDROID, this.CHROME_143_ANDROID,
|
|
96
107
|
this.FIREFOX_133,
|
|
97
108
|
this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
|
|
98
109
|
];
|
|
@@ -205,11 +216,28 @@ class Response {
|
|
|
205
216
|
* @param {Object} data - Response data from native library
|
|
206
217
|
* @param {number} [elapsed=0] - Elapsed time in milliseconds
|
|
207
218
|
*/
|
|
208
|
-
constructor(data, elapsed = 0) {
|
|
219
|
+
constructor(data, elapsed = 0, rawBody = null) {
|
|
209
220
|
this.statusCode = data.status_code || 0;
|
|
210
221
|
this.headers = data.headers || {};
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
241
|
this.finalUrl = data.final_url || "";
|
|
214
242
|
this.protocol = data.protocol || "";
|
|
215
243
|
this.elapsed = elapsed; // milliseconds
|
|
@@ -776,25 +804,37 @@ function getLib() {
|
|
|
776
804
|
const libPath = getLibPath();
|
|
777
805
|
nativeLibHandle = koffi.load(libPath);
|
|
778
806
|
|
|
779
|
-
//
|
|
780
|
-
//
|
|
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
|
+
|
|
781
819
|
lib = {
|
|
782
820
|
httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
|
|
783
821
|
httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
|
|
784
822
|
httpcloak_session_refresh: nativeLibHandle.func("httpcloak_session_refresh", "void", ["int64"]),
|
|
785
|
-
httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol",
|
|
786
|
-
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"]),
|
|
787
825
|
httpcloak_session_fork: nativeLibHandle.func("httpcloak_session_fork", "int64", ["int64"]),
|
|
788
|
-
httpcloak_get: nativeLibHandle.func("httpcloak_get",
|
|
789
|
-
httpcloak_post: nativeLibHandle.func("httpcloak_post",
|
|
790
|
-
httpcloak_request: nativeLibHandle.func("httpcloak_request",
|
|
791
|
-
httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies",
|
|
792
|
-
httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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"]),
|
|
830
|
+
httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
|
|
831
|
+
httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
|
|
832
|
+
httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
|
|
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, []),
|
|
798
838
|
// Async functions
|
|
799
839
|
httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
|
|
800
840
|
httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
|
|
@@ -805,41 +845,41 @@ function getLib() {
|
|
|
805
845
|
httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
|
|
806
846
|
httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
|
|
807
847
|
httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
|
|
808
|
-
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata",
|
|
809
|
-
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"]),
|
|
810
850
|
httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
|
|
811
851
|
// Raw response functions for fast-path (zero-copy)
|
|
812
852
|
httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
|
|
813
853
|
httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
|
|
814
854
|
httpcloak_request_raw: nativeLibHandle.func("httpcloak_request_raw", "int64", ["int64", "str", "void*", "int"]),
|
|
815
|
-
httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata",
|
|
855
|
+
httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", HeapStr, ["int64"]),
|
|
816
856
|
httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
|
|
817
857
|
httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
|
|
818
858
|
httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
|
|
819
859
|
// Combined finalize function (copy + metadata + free in one call)
|
|
820
|
-
httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize",
|
|
860
|
+
httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", HeapStr, ["int64", "void*", "int"]),
|
|
821
861
|
// Session persistence functions
|
|
822
|
-
httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save",
|
|
862
|
+
httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", HeapStr, ["int64", "str"]),
|
|
823
863
|
httpcloak_session_load: nativeLibHandle.func("httpcloak_session_load", "int64", ["str"]),
|
|
824
|
-
httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal",
|
|
864
|
+
httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", HeapStr, ["int64"]),
|
|
825
865
|
httpcloak_session_unmarshal: nativeLibHandle.func("httpcloak_session_unmarshal", "int64", ["str"]),
|
|
826
866
|
// Proxy management functions
|
|
827
|
-
httpcloak_session_set_proxy: nativeLibHandle.func("httpcloak_session_set_proxy",
|
|
828
|
-
httpcloak_session_set_tcp_proxy: nativeLibHandle.func("httpcloak_session_set_tcp_proxy",
|
|
829
|
-
httpcloak_session_set_udp_proxy: nativeLibHandle.func("httpcloak_session_set_udp_proxy",
|
|
830
|
-
httpcloak_session_get_proxy: nativeLibHandle.func("httpcloak_session_get_proxy",
|
|
831
|
-
httpcloak_session_get_tcp_proxy: nativeLibHandle.func("httpcloak_session_get_tcp_proxy",
|
|
832
|
-
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"]),
|
|
833
873
|
// Header order customization
|
|
834
|
-
httpcloak_session_set_header_order: nativeLibHandle.func("httpcloak_session_set_header_order",
|
|
835
|
-
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"]),
|
|
836
876
|
// Local proxy functions
|
|
837
877
|
httpcloak_local_proxy_start: nativeLibHandle.func("httpcloak_local_proxy_start", "int64", ["str"]),
|
|
838
878
|
httpcloak_local_proxy_stop: nativeLibHandle.func("httpcloak_local_proxy_stop", "void", ["int64"]),
|
|
839
879
|
httpcloak_local_proxy_get_port: nativeLibHandle.func("httpcloak_local_proxy_get_port", "int", ["int64"]),
|
|
840
880
|
httpcloak_local_proxy_is_running: nativeLibHandle.func("httpcloak_local_proxy_is_running", "int", ["int64"]),
|
|
841
|
-
httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats",
|
|
842
|
-
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"]),
|
|
843
883
|
httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
|
|
844
884
|
// Session cache callbacks
|
|
845
885
|
httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
|
|
@@ -863,6 +903,23 @@ function getLib() {
|
|
|
863
903
|
// Async cache result functions (called by JS to provide results to Go)
|
|
864
904
|
httpcloak_async_cache_get_result: nativeLibHandle.func("httpcloak_async_cache_get_result", "void", ["int64", "str"]),
|
|
865
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"]),
|
|
866
923
|
};
|
|
867
924
|
}
|
|
868
925
|
return lib;
|
|
@@ -1016,6 +1073,38 @@ function parseResponse(resultPtr, elapsed = 0) {
|
|
|
1016
1073
|
return new Response(data, elapsed);
|
|
1017
1074
|
}
|
|
1018
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
|
+
|
|
1019
1108
|
/**
|
|
1020
1109
|
* Add query parameters to URL
|
|
1021
1110
|
*/
|
|
@@ -1162,7 +1251,7 @@ function version() {
|
|
|
1162
1251
|
/**
|
|
1163
1252
|
* Get available browser presets with their supported protocols.
|
|
1164
1253
|
* Returns an object mapping preset names to their info:
|
|
1165
|
-
* { "chrome-
|
|
1254
|
+
* { "chrome-146": { protocols: ["h1", "h2", "h3"] }, ... }
|
|
1166
1255
|
*/
|
|
1167
1256
|
function availablePresets() {
|
|
1168
1257
|
const nativeLib = getLib();
|
|
@@ -1228,7 +1317,7 @@ class Session {
|
|
|
1228
1317
|
/**
|
|
1229
1318
|
* Create a new session
|
|
1230
1319
|
* @param {Object} options - Session options
|
|
1231
|
-
* @param {string} [options.preset="chrome-
|
|
1320
|
+
* @param {string} [options.preset="chrome-146"] - Browser preset to use
|
|
1232
1321
|
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
|
|
1233
1322
|
* @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
|
|
1234
1323
|
* @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
|
|
@@ -1237,8 +1326,8 @@ class Session {
|
|
|
1237
1326
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
1238
1327
|
* @param {boolean} [options.allowRedirects=true] - Follow redirects
|
|
1239
1328
|
* @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
|
|
1240
|
-
* @param {number} [options.retry=
|
|
1241
|
-
* @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)
|
|
1242
1331
|
* @param {number} [options.retryWaitMin=500] - Minimum wait time between retries in milliseconds
|
|
1243
1332
|
* @param {number} [options.retryWaitMax=10000] - Maximum wait time between retries in milliseconds
|
|
1244
1333
|
* @param {Array} [options.auth] - Default auth [username, password] for all requests
|
|
@@ -1249,7 +1338,7 @@ class Session {
|
|
|
1249
1338
|
*/
|
|
1250
1339
|
constructor(options = {}) {
|
|
1251
1340
|
const {
|
|
1252
|
-
preset = "chrome-
|
|
1341
|
+
preset = "chrome-146",
|
|
1253
1342
|
proxy = null,
|
|
1254
1343
|
tcpProxy = null,
|
|
1255
1344
|
udpProxy = null,
|
|
@@ -1258,7 +1347,7 @@ class Session {
|
|
|
1258
1347
|
verify = true,
|
|
1259
1348
|
allowRedirects = true,
|
|
1260
1349
|
maxRedirects = 10,
|
|
1261
|
-
retry =
|
|
1350
|
+
retry = 0,
|
|
1262
1351
|
retryOnStatus = null,
|
|
1263
1352
|
retryWaitMin = 500,
|
|
1264
1353
|
retryWaitMax = 10000,
|
|
@@ -1279,6 +1368,7 @@ class Session {
|
|
|
1279
1368
|
tcpMss = null,
|
|
1280
1369
|
tcpWindowSize = null,
|
|
1281
1370
|
tcpWindowScale = null,
|
|
1371
|
+
tcpDf = null,
|
|
1282
1372
|
} = options;
|
|
1283
1373
|
|
|
1284
1374
|
this._lib = getLib();
|
|
@@ -1366,6 +1456,9 @@ class Session {
|
|
|
1366
1456
|
if (tcpWindowScale != null) {
|
|
1367
1457
|
config.tcp_window_scale = tcpWindowScale;
|
|
1368
1458
|
}
|
|
1459
|
+
if (tcpDf != null) {
|
|
1460
|
+
config.tcp_df = tcpDf;
|
|
1461
|
+
}
|
|
1369
1462
|
|
|
1370
1463
|
this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
|
|
1371
1464
|
|
|
@@ -1505,7 +1598,7 @@ class Session {
|
|
|
1505
1598
|
* @returns {Response} Response object
|
|
1506
1599
|
*/
|
|
1507
1600
|
getSync(url, options = {}) {
|
|
1508
|
-
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
1601
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1509
1602
|
|
|
1510
1603
|
url = addParamsToUrl(url, params);
|
|
1511
1604
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1519,12 +1612,18 @@ class Session {
|
|
|
1519
1612
|
if (mergedHeaders) {
|
|
1520
1613
|
reqOptions.headers = mergedHeaders;
|
|
1521
1614
|
}
|
|
1615
|
+
if (fetchMode) {
|
|
1616
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1617
|
+
}
|
|
1522
1618
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1523
1619
|
|
|
1524
1620
|
const startTime = Date.now();
|
|
1525
|
-
const
|
|
1621
|
+
const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
|
|
1526
1622
|
const elapsed = Date.now() - startTime;
|
|
1527
|
-
|
|
1623
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1624
|
+
throw new HTTPCloakError("Request failed");
|
|
1625
|
+
}
|
|
1626
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1528
1627
|
}
|
|
1529
1628
|
|
|
1530
1629
|
/**
|
|
@@ -1545,38 +1644,36 @@ class Session {
|
|
|
1545
1644
|
* @returns {Response} Response object
|
|
1546
1645
|
*/
|
|
1547
1646
|
postSync(url, options = {}) {
|
|
1548
|
-
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;
|
|
1549
1648
|
|
|
1550
1649
|
url = addParamsToUrl(url, params);
|
|
1551
1650
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1552
1651
|
|
|
1553
|
-
//
|
|
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;
|
|
1554
1655
|
if (files !== null) {
|
|
1555
1656
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1556
1657
|
const multipart = encodeMultipart(formData, files);
|
|
1557
|
-
|
|
1658
|
+
bodyBuffer = multipart.body;
|
|
1558
1659
|
mergedHeaders = mergedHeaders || {};
|
|
1559
1660
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
else if (json !== null) {
|
|
1563
|
-
body = JSON.stringify(json);
|
|
1661
|
+
} else if (json !== null) {
|
|
1662
|
+
bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
|
|
1564
1663
|
mergedHeaders = mergedHeaders || {};
|
|
1565
1664
|
if (!mergedHeaders["Content-Type"]) {
|
|
1566
1665
|
mergedHeaders["Content-Type"] = "application/json";
|
|
1567
1666
|
}
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
else if (data !== null && typeof data === "object") {
|
|
1571
|
-
body = new URLSearchParams(data).toString();
|
|
1667
|
+
} else if (data !== null && typeof data === "object") {
|
|
1668
|
+
bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
|
|
1572
1669
|
mergedHeaders = mergedHeaders || {};
|
|
1573
1670
|
if (!mergedHeaders["Content-Type"]) {
|
|
1574
1671
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1575
1672
|
}
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
else if (
|
|
1579
|
-
|
|
1673
|
+
} else if (Buffer.isBuffer(body)) {
|
|
1674
|
+
bodyBuffer = body;
|
|
1675
|
+
} else if (typeof body === "string") {
|
|
1676
|
+
bodyBuffer = Buffer.from(body, "utf8");
|
|
1580
1677
|
}
|
|
1581
1678
|
|
|
1582
1679
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1589,12 +1686,21 @@ class Session {
|
|
|
1589
1686
|
if (mergedHeaders) {
|
|
1590
1687
|
reqOptions.headers = mergedHeaders;
|
|
1591
1688
|
}
|
|
1689
|
+
if (fetchMode) {
|
|
1690
|
+
reqOptions.fetch_mode = fetchMode;
|
|
1691
|
+
}
|
|
1592
1692
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1593
1693
|
|
|
1694
|
+
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1695
|
+
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
1696
|
+
|
|
1594
1697
|
const startTime = Date.now();
|
|
1595
|
-
const
|
|
1698
|
+
const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, bodyPtr, bodyLen, optionsJson);
|
|
1596
1699
|
const elapsed = Date.now() - startTime;
|
|
1597
|
-
|
|
1700
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1701
|
+
throw new HTTPCloakError("Request failed");
|
|
1702
|
+
}
|
|
1703
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1598
1704
|
}
|
|
1599
1705
|
|
|
1600
1706
|
/**
|
|
@@ -1607,38 +1713,35 @@ class Session {
|
|
|
1607
1713
|
* @returns {Response} Response object
|
|
1608
1714
|
*/
|
|
1609
1715
|
requestSync(method, url, options = {}) {
|
|
1610
|
-
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;
|
|
1611
1717
|
|
|
1612
1718
|
url = addParamsToUrl(url, params);
|
|
1613
1719
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1614
1720
|
|
|
1615
|
-
//
|
|
1721
|
+
// Normalize body to a Buffer so the raw path can pass (ptr, len).
|
|
1722
|
+
let bodyBuffer = null;
|
|
1616
1723
|
if (files !== null) {
|
|
1617
1724
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1618
1725
|
const multipart = encodeMultipart(formData, files);
|
|
1619
|
-
|
|
1726
|
+
bodyBuffer = multipart.body;
|
|
1620
1727
|
mergedHeaders = mergedHeaders || {};
|
|
1621
1728
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
else if (json !== null) {
|
|
1625
|
-
body = JSON.stringify(json);
|
|
1729
|
+
} else if (json !== null) {
|
|
1730
|
+
bodyBuffer = Buffer.from(JSON.stringify(json), "utf8");
|
|
1626
1731
|
mergedHeaders = mergedHeaders || {};
|
|
1627
1732
|
if (!mergedHeaders["Content-Type"]) {
|
|
1628
1733
|
mergedHeaders["Content-Type"] = "application/json";
|
|
1629
1734
|
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
else if (data !== null && typeof data === "object") {
|
|
1633
|
-
body = new URLSearchParams(data).toString();
|
|
1735
|
+
} else if (data !== null && typeof data === "object") {
|
|
1736
|
+
bodyBuffer = Buffer.from(new URLSearchParams(data).toString(), "utf8");
|
|
1634
1737
|
mergedHeaders = mergedHeaders || {};
|
|
1635
1738
|
if (!mergedHeaders["Content-Type"]) {
|
|
1636
1739
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1637
1740
|
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
else if (
|
|
1641
|
-
|
|
1741
|
+
} else if (Buffer.isBuffer(body)) {
|
|
1742
|
+
bodyBuffer = body;
|
|
1743
|
+
} else if (typeof body === "string") {
|
|
1744
|
+
bodyBuffer = Buffer.from(body, "utf8");
|
|
1642
1745
|
}
|
|
1643
1746
|
|
|
1644
1747
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1651,16 +1754,24 @@ class Session {
|
|
|
1651
1754
|
url,
|
|
1652
1755
|
};
|
|
1653
1756
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1654
|
-
if (body) requestConfig.body = body;
|
|
1655
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;
|
|
1656
1762
|
|
|
1657
1763
|
const startTime = Date.now();
|
|
1658
|
-
const
|
|
1764
|
+
const responseHandle = this._lib.httpcloak_request_raw(
|
|
1659
1765
|
this._handle,
|
|
1660
|
-
JSON.stringify(requestConfig)
|
|
1766
|
+
JSON.stringify(requestConfig),
|
|
1767
|
+
bodyPtr,
|
|
1768
|
+
bodyLen
|
|
1661
1769
|
);
|
|
1662
1770
|
const elapsed = Date.now() - startTime;
|
|
1663
|
-
|
|
1771
|
+
if (responseHandle < 0 || responseHandle === 0 || responseHandle === 0n) {
|
|
1772
|
+
throw new HTTPCloakError("Request failed");
|
|
1773
|
+
}
|
|
1774
|
+
return parseRawResponse(this._lib, responseHandle, elapsed);
|
|
1664
1775
|
}
|
|
1665
1776
|
|
|
1666
1777
|
// ===========================================================================
|
|
@@ -1675,7 +1786,7 @@ class Session {
|
|
|
1675
1786
|
* @returns {Promise<Response>} Response object
|
|
1676
1787
|
*/
|
|
1677
1788
|
get(url, options = {}) {
|
|
1678
|
-
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;
|
|
1679
1790
|
|
|
1680
1791
|
url = addParamsToUrl(url, params);
|
|
1681
1792
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1689,6 +1800,15 @@ class Session {
|
|
|
1689
1800
|
if (mergedHeaders) {
|
|
1690
1801
|
reqOptions.headers = mergedHeaders;
|
|
1691
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
|
+
}
|
|
1692
1812
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1693
1813
|
|
|
1694
1814
|
// Register async request with callback manager
|
|
@@ -1709,7 +1829,7 @@ class Session {
|
|
|
1709
1829
|
* @returns {Promise<Response>} Response object
|
|
1710
1830
|
*/
|
|
1711
1831
|
post(url, options = {}) {
|
|
1712
|
-
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;
|
|
1713
1833
|
|
|
1714
1834
|
url = addParamsToUrl(url, params);
|
|
1715
1835
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1753,6 +1873,13 @@ class Session {
|
|
|
1753
1873
|
if (mergedHeaders) {
|
|
1754
1874
|
reqOptions.headers = mergedHeaders;
|
|
1755
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
|
+
}
|
|
1756
1883
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1757
1884
|
|
|
1758
1885
|
// Register async request with callback manager
|
|
@@ -1774,7 +1901,7 @@ class Session {
|
|
|
1774
1901
|
* @returns {Promise<Response>} Response object
|
|
1775
1902
|
*/
|
|
1776
1903
|
request(method, url, options = {}) {
|
|
1777
|
-
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;
|
|
1778
1905
|
|
|
1779
1906
|
url = addParamsToUrl(url, params);
|
|
1780
1907
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1820,6 +1947,7 @@ class Session {
|
|
|
1820
1947
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1821
1948
|
if (body) requestConfig.body = body;
|
|
1822
1949
|
if (timeout) requestConfig.timeout = timeout;
|
|
1950
|
+
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1823
1951
|
|
|
1824
1952
|
// Register async request with callback manager
|
|
1825
1953
|
const manager = getAsyncManager();
|
|
@@ -1871,58 +1999,101 @@ class Session {
|
|
|
1871
1999
|
// ===========================================================================
|
|
1872
2000
|
|
|
1873
2001
|
/**
|
|
1874
|
-
* Get all cookies
|
|
1875
|
-
* @returns {
|
|
2002
|
+
* Get all cookies with full metadata (domain, path, expiry, flags).
|
|
2003
|
+
* @returns {Cookie[]} Array of Cookie objects
|
|
1876
2004
|
*/
|
|
1877
|
-
|
|
2005
|
+
getCookiesDetailed() {
|
|
1878
2006
|
const resultPtr = this._lib.httpcloak_get_cookies(this._handle);
|
|
1879
2007
|
const result = resultToString(resultPtr);
|
|
1880
2008
|
if (result) {
|
|
1881
|
-
|
|
2009
|
+
const parsed = JSON.parse(result);
|
|
2010
|
+
return parsed.map(c => new Cookie(c));
|
|
1882
2011
|
}
|
|
1883
|
-
return
|
|
2012
|
+
return [];
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
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[]}
|
|
2022
|
+
*/
|
|
2023
|
+
getCookies() {
|
|
2024
|
+
return this.getCookiesDetailed();
|
|
1884
2025
|
}
|
|
1885
2026
|
|
|
1886
2027
|
/**
|
|
1887
|
-
* Get a specific cookie by name
|
|
2028
|
+
* Get a specific cookie by name with full metadata.
|
|
1888
2029
|
* @param {string} name - Cookie name
|
|
1889
|
-
* @returns {
|
|
2030
|
+
* @returns {Cookie|null} Cookie object or null if not found
|
|
2031
|
+
*/
|
|
2032
|
+
getCookieDetailed(name) {
|
|
2033
|
+
const cookies = this.getCookiesDetailed();
|
|
2034
|
+
return cookies.find(c => c.name === name) || null;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
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`.
|
|
2042
|
+
* @param {string} name - Cookie name
|
|
2043
|
+
* @returns {Cookie|null}
|
|
1890
2044
|
*/
|
|
1891
2045
|
getCookie(name) {
|
|
1892
|
-
|
|
1893
|
-
return cookies[name] || null;
|
|
2046
|
+
return this.getCookieDetailed(name);
|
|
1894
2047
|
}
|
|
1895
2048
|
|
|
1896
2049
|
/**
|
|
1897
2050
|
* Set a cookie in the session
|
|
1898
2051
|
* @param {string} name - Cookie name
|
|
1899
2052
|
* @param {string} value - Cookie value
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2053
|
+
* @param {Object} [options] - Cookie options
|
|
2054
|
+
* @param {string} [options.domain] - Cookie domain
|
|
2055
|
+
* @param {string} [options.path] - Cookie path (default: "/")
|
|
2056
|
+
* @param {boolean} [options.secure] - Secure flag
|
|
2057
|
+
* @param {boolean} [options.httpOnly] - HttpOnly flag
|
|
2058
|
+
* @param {string} [options.sameSite] - SameSite attribute (Strict, Lax, None)
|
|
2059
|
+
* @param {number} [options.maxAge] - Max age in seconds (0 means not set)
|
|
2060
|
+
* @param {string} [options.expires] - Expiration date (RFC1123 format)
|
|
2061
|
+
*/
|
|
2062
|
+
setCookie(name, value, options = {}) {
|
|
2063
|
+
const cookie = {
|
|
2064
|
+
name,
|
|
2065
|
+
value,
|
|
2066
|
+
domain: options.domain || "",
|
|
2067
|
+
path: options.path || "/",
|
|
2068
|
+
secure: options.secure || false,
|
|
2069
|
+
http_only: options.httpOnly || false,
|
|
2070
|
+
same_site: options.sameSite || "",
|
|
2071
|
+
max_age: options.maxAge || 0,
|
|
2072
|
+
expires: options.expires || "",
|
|
2073
|
+
};
|
|
2074
|
+
this._lib.httpcloak_set_cookie(this._handle, JSON.stringify(cookie));
|
|
1903
2075
|
}
|
|
1904
2076
|
|
|
1905
2077
|
/**
|
|
1906
2078
|
* Delete a specific cookie by name
|
|
1907
2079
|
* @param {string} name - Cookie name to delete
|
|
2080
|
+
* @param {string} [domain] - Domain to delete from (omit to delete from all domains)
|
|
1908
2081
|
*/
|
|
1909
|
-
deleteCookie(name) {
|
|
1910
|
-
|
|
1911
|
-
this._lib.httpcloak_set_cookie(this._handle, name, "");
|
|
2082
|
+
deleteCookie(name, domain = "") {
|
|
2083
|
+
this._lib.httpcloak_delete_cookie(this._handle, name, domain);
|
|
1912
2084
|
}
|
|
1913
2085
|
|
|
1914
2086
|
/**
|
|
1915
2087
|
* Clear all cookies from the session
|
|
1916
2088
|
*/
|
|
1917
2089
|
clearCookies() {
|
|
1918
|
-
|
|
1919
|
-
for (const name of Object.keys(cookies)) {
|
|
1920
|
-
this.deleteCookie(name);
|
|
1921
|
-
}
|
|
2090
|
+
this._lib.httpcloak_clear_cookies(this._handle);
|
|
1922
2091
|
}
|
|
1923
2092
|
|
|
1924
2093
|
/**
|
|
1925
|
-
* Get cookies as a property
|
|
2094
|
+
* Get cookies as a property.
|
|
2095
|
+
* @deprecated This property will return Cookie[] with full metadata in a future release.
|
|
2096
|
+
* @returns {Object} Cookies as key-value pairs
|
|
1926
2097
|
*/
|
|
1927
2098
|
get cookies() {
|
|
1928
2099
|
return this.getCookies();
|
|
@@ -2093,7 +2264,7 @@ class Session {
|
|
|
2093
2264
|
* @param {string} path - Path to save the session file
|
|
2094
2265
|
*
|
|
2095
2266
|
* Example:
|
|
2096
|
-
* const session = new httpcloak.Session({ preset: "chrome-
|
|
2267
|
+
* const session = new httpcloak.Session({ preset: "chrome-146" });
|
|
2097
2268
|
* await session.get("https://example.com"); // Acquire cookies
|
|
2098
2269
|
* session.save("session.json");
|
|
2099
2270
|
*
|
|
@@ -2731,7 +2902,7 @@ let _defaultConfig = {};
|
|
|
2731
2902
|
/**
|
|
2732
2903
|
* Configure defaults for module-level functions
|
|
2733
2904
|
* @param {Object} options - Configuration options
|
|
2734
|
-
* @param {string} [options.preset="chrome-
|
|
2905
|
+
* @param {string} [options.preset="chrome-146"] - Browser preset
|
|
2735
2906
|
* @param {Object} [options.headers] - Default headers
|
|
2736
2907
|
* @param {Array} [options.auth] - Default basic auth [username, password]
|
|
2737
2908
|
* @param {string} [options.proxy] - Proxy URL
|
|
@@ -2740,12 +2911,12 @@ let _defaultConfig = {};
|
|
|
2740
2911
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
2741
2912
|
* @param {boolean} [options.allowRedirects=true] - Follow redirects
|
|
2742
2913
|
* @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
|
|
2743
|
-
* @param {number} [options.retry=
|
|
2744
|
-
* @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)
|
|
2745
2916
|
*/
|
|
2746
2917
|
function configure(options = {}) {
|
|
2747
2918
|
const {
|
|
2748
|
-
preset = "chrome-
|
|
2919
|
+
preset = "chrome-146",
|
|
2749
2920
|
headers = null,
|
|
2750
2921
|
auth = null,
|
|
2751
2922
|
proxy = null,
|
|
@@ -2754,7 +2925,7 @@ function configure(options = {}) {
|
|
|
2754
2925
|
verify = true,
|
|
2755
2926
|
allowRedirects = true,
|
|
2756
2927
|
maxRedirects = 10,
|
|
2757
|
-
retry =
|
|
2928
|
+
retry = 0,
|
|
2758
2929
|
retryOnStatus = null,
|
|
2759
2930
|
} = options;
|
|
2760
2931
|
|
|
@@ -2803,14 +2974,14 @@ function configure(options = {}) {
|
|
|
2803
2974
|
*/
|
|
2804
2975
|
function _getDefaultSession() {
|
|
2805
2976
|
if (!_defaultSession) {
|
|
2806
|
-
const preset = _defaultConfig.preset || "chrome-
|
|
2977
|
+
const preset = _defaultConfig.preset || "chrome-146";
|
|
2807
2978
|
const proxy = _defaultConfig.proxy || null;
|
|
2808
2979
|
const timeout = _defaultConfig.timeout || 30;
|
|
2809
2980
|
const httpVersion = _defaultConfig.httpVersion || "auto";
|
|
2810
2981
|
const verify = _defaultConfig.verify !== undefined ? _defaultConfig.verify : true;
|
|
2811
2982
|
const allowRedirects = _defaultConfig.allowRedirects !== undefined ? _defaultConfig.allowRedirects : true;
|
|
2812
2983
|
const maxRedirects = _defaultConfig.maxRedirects || 10;
|
|
2813
|
-
const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry :
|
|
2984
|
+
const retry = _defaultConfig.retry !== undefined ? _defaultConfig.retry : 0;
|
|
2814
2985
|
const retryOnStatus = _defaultConfig.retryOnStatus || null;
|
|
2815
2986
|
const headers = _defaultConfig.headers || {};
|
|
2816
2987
|
|
|
@@ -2906,7 +3077,7 @@ function request(method, url, options = {}) {
|
|
|
2906
3077
|
* Without registration, cache callbacks will not be triggered for that session.
|
|
2907
3078
|
*
|
|
2908
3079
|
* @example
|
|
2909
|
-
* const proxy = new LocalProxy({ preset: "chrome-
|
|
3080
|
+
* const proxy = new LocalProxy({ preset: "chrome-146", tlsOnly: true });
|
|
2910
3081
|
* console.log(`Proxy running on ${proxy.proxyUrl}`);
|
|
2911
3082
|
*
|
|
2912
3083
|
* // Use with any HTTP client pointing to the proxy
|
|
@@ -2935,7 +3106,7 @@ class LocalProxy {
|
|
|
2935
3106
|
* Create and start a local HTTP proxy server.
|
|
2936
3107
|
* @param {Object} options - Proxy configuration options
|
|
2937
3108
|
* @param {number} [options.port=0] - Port to listen on (0 = auto-select)
|
|
2938
|
-
* @param {string} [options.preset="chrome-
|
|
3109
|
+
* @param {string} [options.preset="chrome-146"] - Browser fingerprint preset
|
|
2939
3110
|
* @param {number} [options.timeout=30] - Request timeout in seconds
|
|
2940
3111
|
* @param {number} [options.maxConnections=1000] - Maximum concurrent connections
|
|
2941
3112
|
* @param {string} [options.tcpProxy] - Default upstream TCP proxy URL
|
|
@@ -2945,7 +3116,7 @@ class LocalProxy {
|
|
|
2945
3116
|
constructor(options = {}) {
|
|
2946
3117
|
const {
|
|
2947
3118
|
port = 0,
|
|
2948
|
-
preset = "chrome-
|
|
3119
|
+
preset = "chrome-146",
|
|
2949
3120
|
timeout = 30,
|
|
2950
3121
|
maxConnections = 1000,
|
|
2951
3122
|
tcpProxy = null,
|
|
@@ -3457,10 +3628,247 @@ function clearSessionCache() {
|
|
|
3457
3628
|
}
|
|
3458
3629
|
|
|
3459
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
|
+
|
|
3460
3867
|
module.exports = {
|
|
3461
3868
|
// Classes
|
|
3462
3869
|
Session,
|
|
3463
3870
|
LocalProxy,
|
|
3871
|
+
PresetPool,
|
|
3464
3872
|
Response,
|
|
3465
3873
|
FastResponse,
|
|
3466
3874
|
StreamResponse,
|
|
@@ -3470,6 +3878,11 @@ module.exports = {
|
|
|
3470
3878
|
SessionCacheBackend,
|
|
3471
3879
|
// Presets
|
|
3472
3880
|
Preset,
|
|
3881
|
+
// Custom preset loading
|
|
3882
|
+
loadPreset,
|
|
3883
|
+
loadPresetFromJSON,
|
|
3884
|
+
unregisterPreset,
|
|
3885
|
+
describePreset,
|
|
3473
3886
|
// Configuration
|
|
3474
3887
|
configure,
|
|
3475
3888
|
configureSessionCache,
|