httpcloak 1.6.6 → 1.6.8-beta.1
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 +297 -28
- package/lib/index.js +564 -45
- package/lib/index.mjs +7 -0
- package/npm/darwin-arm64/package.json +1 -1
- package/npm/darwin-x64/package.json +1 -1
- package/npm/linux-arm64/package.json +1 -1
- package/npm/linux-x64/package.json +1 -1
- package/npm/win32-x64/package.json +1 -1
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -21,91 +21,143 @@ class HTTPCloakError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Available browser presets for TLS fingerprinting.
|
|
24
|
+
* Available browser presets for TLS fingerprinting. Mirrors the runtime
|
|
25
|
+
* registry shipped by the linked libhttpcloak. Use Preset.CHROME_LATEST to
|
|
26
|
+
* auto-track newest stable Chrome, or pin to a specific major version for
|
|
27
|
+
* reproducibility.
|
|
25
28
|
*
|
|
26
|
-
* Use these constants instead of typing preset strings manually:
|
|
27
29
|
* const httpcloak = require("httpcloak");
|
|
28
|
-
* httpcloak.configure({ preset: httpcloak.Preset.
|
|
30
|
+
* httpcloak.configure({ preset: httpcloak.Preset.CHROME_LATEST });
|
|
29
31
|
*
|
|
30
|
-
*
|
|
31
|
-
* const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_133 });
|
|
32
|
+
* const session = new httpcloak.Session({ preset: httpcloak.Preset.FIREFOX_LATEST });
|
|
32
33
|
*/
|
|
33
34
|
const Preset = {
|
|
34
|
-
// Chrome
|
|
35
|
+
// Chrome latest (auto-resolves to newest shipped Chrome)
|
|
36
|
+
CHROME_LATEST: "chrome-latest",
|
|
37
|
+
CHROME_LATEST_WINDOWS: "chrome-latest-windows",
|
|
38
|
+
CHROME_LATEST_LINUX: "chrome-latest-linux",
|
|
39
|
+
CHROME_LATEST_MACOS: "chrome-latest-macos",
|
|
40
|
+
CHROME_LATEST_IOS: "chrome-latest-ios",
|
|
41
|
+
CHROME_LATEST_ANDROID: "chrome-latest-android",
|
|
42
|
+
|
|
43
|
+
// Chrome 149 (desktop; wire fingerprint identical to 148)
|
|
44
|
+
CHROME_149: "chrome-149",
|
|
45
|
+
CHROME_149_WINDOWS: "chrome-149-windows",
|
|
46
|
+
CHROME_149_LINUX: "chrome-149-linux",
|
|
47
|
+
CHROME_149_MACOS: "chrome-149-macos",
|
|
48
|
+
|
|
49
|
+
// Chrome 148
|
|
50
|
+
CHROME_148: "chrome-148",
|
|
51
|
+
CHROME_148_WINDOWS: "chrome-148-windows",
|
|
52
|
+
CHROME_148_LINUX: "chrome-148-linux",
|
|
53
|
+
CHROME_148_MACOS: "chrome-148-macos",
|
|
54
|
+
CHROME_148_IOS: "chrome-148-ios",
|
|
55
|
+
CHROME_148_ANDROID: "chrome-148-android",
|
|
56
|
+
|
|
57
|
+
// Chrome 147
|
|
58
|
+
CHROME_147: "chrome-147",
|
|
59
|
+
CHROME_147_WINDOWS: "chrome-147-windows",
|
|
60
|
+
CHROME_147_LINUX: "chrome-147-linux",
|
|
61
|
+
CHROME_147_MACOS: "chrome-147-macos",
|
|
62
|
+
CHROME_147_IOS: "chrome-147-ios",
|
|
63
|
+
CHROME_147_ANDROID: "chrome-147-android",
|
|
64
|
+
|
|
65
|
+
// Chrome 146
|
|
35
66
|
CHROME_146: "chrome-146",
|
|
36
67
|
CHROME_146_WINDOWS: "chrome-146-windows",
|
|
37
68
|
CHROME_146_LINUX: "chrome-146-linux",
|
|
38
69
|
CHROME_146_MACOS: "chrome-146-macos",
|
|
70
|
+
CHROME_146_IOS: "chrome-146-ios",
|
|
71
|
+
CHROME_146_ANDROID: "chrome-146-android",
|
|
39
72
|
|
|
40
73
|
// Chrome 145
|
|
41
74
|
CHROME_145: "chrome-145",
|
|
42
75
|
CHROME_145_WINDOWS: "chrome-145-windows",
|
|
43
76
|
CHROME_145_LINUX: "chrome-145-linux",
|
|
44
77
|
CHROME_145_MACOS: "chrome-145-macos",
|
|
78
|
+
CHROME_145_IOS: "chrome-145-ios",
|
|
79
|
+
CHROME_145_ANDROID: "chrome-145-android",
|
|
45
80
|
|
|
46
81
|
// Chrome 144
|
|
47
82
|
CHROME_144: "chrome-144",
|
|
48
83
|
CHROME_144_WINDOWS: "chrome-144-windows",
|
|
49
84
|
CHROME_144_LINUX: "chrome-144-linux",
|
|
50
85
|
CHROME_144_MACOS: "chrome-144-macos",
|
|
86
|
+
CHROME_144_IOS: "chrome-144-ios",
|
|
87
|
+
CHROME_144_ANDROID: "chrome-144-android",
|
|
51
88
|
|
|
52
89
|
// Chrome 143
|
|
53
90
|
CHROME_143: "chrome-143",
|
|
54
91
|
CHROME_143_WINDOWS: "chrome-143-windows",
|
|
55
92
|
CHROME_143_LINUX: "chrome-143-linux",
|
|
56
93
|
CHROME_143_MACOS: "chrome-143-macos",
|
|
94
|
+
CHROME_143_IOS: "chrome-143-ios",
|
|
95
|
+
CHROME_143_ANDROID: "chrome-143-android",
|
|
57
96
|
|
|
58
|
-
// Chrome
|
|
97
|
+
// Older Chrome (H1/H2 only, no H3)
|
|
59
98
|
CHROME_141: "chrome-141",
|
|
60
|
-
|
|
61
|
-
// Chrome 133
|
|
62
99
|
CHROME_133: "chrome-133",
|
|
63
100
|
|
|
64
|
-
// Mobile Chrome
|
|
65
|
-
CHROME_143_IOS: "chrome-143-ios",
|
|
66
|
-
CHROME_144_IOS: "chrome-144-ios",
|
|
67
|
-
CHROME_145_IOS: "chrome-145-ios",
|
|
68
|
-
CHROME_146_IOS: "chrome-146-ios",
|
|
69
|
-
CHROME_143_ANDROID: "chrome-143-android",
|
|
70
|
-
CHROME_144_ANDROID: "chrome-144-android",
|
|
71
|
-
CHROME_145_ANDROID: "chrome-145-android",
|
|
72
|
-
CHROME_146_ANDROID: "chrome-146-android",
|
|
73
|
-
|
|
74
101
|
// Firefox
|
|
102
|
+
FIREFOX_LATEST: "firefox-latest",
|
|
103
|
+
FIREFOX_148: "firefox-148",
|
|
75
104
|
FIREFOX_133: "firefox-133",
|
|
76
105
|
|
|
77
106
|
// Safari (desktop and mobile)
|
|
107
|
+
SAFARI_LATEST: "safari-latest",
|
|
78
108
|
SAFARI_18: "safari-18",
|
|
79
109
|
SAFARI_17_IOS: "safari-17-ios",
|
|
80
110
|
SAFARI_18_IOS: "safari-18-ios",
|
|
111
|
+
SAFARI_LATEST_IOS: "safari-latest-ios",
|
|
81
112
|
|
|
82
|
-
// Backwards compatibility aliases (old naming
|
|
113
|
+
// Backwards compatibility aliases (old "ios-chrome" / "android-chrome" naming)
|
|
83
114
|
IOS_CHROME_143: "chrome-143-ios",
|
|
84
115
|
IOS_CHROME_144: "chrome-144-ios",
|
|
85
116
|
IOS_CHROME_145: "chrome-145-ios",
|
|
86
117
|
IOS_CHROME_146: "chrome-146-ios",
|
|
118
|
+
IOS_CHROME_147: "chrome-147-ios",
|
|
119
|
+
IOS_CHROME_148: "chrome-148-ios",
|
|
120
|
+
IOS_CHROME_LATEST: "chrome-latest-ios",
|
|
87
121
|
ANDROID_CHROME_143: "chrome-143-android",
|
|
88
122
|
ANDROID_CHROME_144: "chrome-144-android",
|
|
89
123
|
ANDROID_CHROME_145: "chrome-145-android",
|
|
90
124
|
ANDROID_CHROME_146: "chrome-146-android",
|
|
125
|
+
ANDROID_CHROME_147: "chrome-147-android",
|
|
126
|
+
ANDROID_CHROME_148: "chrome-148-android",
|
|
127
|
+
ANDROID_CHROME_LATEST: "chrome-latest-android",
|
|
91
128
|
IOS_SAFARI_17: "safari-17-ios",
|
|
92
129
|
IOS_SAFARI_18: "safari-18-ios",
|
|
130
|
+
IOS_SAFARI_LATEST: "safari-latest-ios",
|
|
93
131
|
|
|
94
132
|
/**
|
|
95
|
-
* Get all
|
|
96
|
-
*
|
|
133
|
+
* Get all built-in preset names known to this binding version.
|
|
134
|
+
*
|
|
135
|
+
* For the authoritative live list (which may include custom presets
|
|
136
|
+
* loaded at runtime), call `availablePresets()` instead.
|
|
137
|
+
*
|
|
138
|
+
* @returns {string[]} List of preset names
|
|
97
139
|
*/
|
|
98
140
|
all() {
|
|
99
141
|
return [
|
|
142
|
+
this.CHROME_LATEST, this.CHROME_LATEST_WINDOWS, this.CHROME_LATEST_LINUX,
|
|
143
|
+
this.CHROME_LATEST_MACOS, this.CHROME_LATEST_IOS, this.CHROME_LATEST_ANDROID,
|
|
144
|
+
this.CHROME_149, this.CHROME_149_WINDOWS, this.CHROME_149_LINUX, this.CHROME_149_MACOS,
|
|
145
|
+
this.CHROME_148, this.CHROME_148_WINDOWS, this.CHROME_148_LINUX, this.CHROME_148_MACOS,
|
|
146
|
+
this.CHROME_148_IOS, this.CHROME_148_ANDROID,
|
|
147
|
+
this.CHROME_147, this.CHROME_147_WINDOWS, this.CHROME_147_LINUX, this.CHROME_147_MACOS,
|
|
148
|
+
this.CHROME_147_IOS, this.CHROME_147_ANDROID,
|
|
100
149
|
this.CHROME_146, this.CHROME_146_WINDOWS, this.CHROME_146_LINUX, this.CHROME_146_MACOS,
|
|
150
|
+
this.CHROME_146_IOS, this.CHROME_146_ANDROID,
|
|
101
151
|
this.CHROME_145, this.CHROME_145_WINDOWS, this.CHROME_145_LINUX, this.CHROME_145_MACOS,
|
|
152
|
+
this.CHROME_145_IOS, this.CHROME_145_ANDROID,
|
|
102
153
|
this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
|
|
154
|
+
this.CHROME_144_IOS, this.CHROME_144_ANDROID,
|
|
103
155
|
this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
|
|
156
|
+
this.CHROME_143_IOS, this.CHROME_143_ANDROID,
|
|
104
157
|
this.CHROME_141, this.CHROME_133,
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
108
|
-
this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
|
|
158
|
+
this.FIREFOX_LATEST, this.FIREFOX_148, this.FIREFOX_133,
|
|
159
|
+
this.SAFARI_LATEST, this.SAFARI_18, this.SAFARI_17_IOS, this.SAFARI_18_IOS,
|
|
160
|
+
this.SAFARI_LATEST_IOS,
|
|
109
161
|
];
|
|
110
162
|
},
|
|
111
163
|
};
|
|
@@ -830,6 +882,22 @@ function getLib() {
|
|
|
830
882
|
httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str"]),
|
|
831
883
|
httpcloak_delete_cookie: nativeLibHandle.func("httpcloak_delete_cookie", "void", ["int64", "str", "str"]),
|
|
832
884
|
httpcloak_clear_cookies: nativeLibHandle.func("httpcloak_clear_cookies", "void", ["int64"]),
|
|
885
|
+
httpcloak_session_clear_cache: nativeLibHandle.func("httpcloak_session_clear_cache", "void", ["int64"]),
|
|
886
|
+
httpcloak_session_stats: nativeLibHandle.func("httpcloak_session_stats", HeapStr, ["int64"]),
|
|
887
|
+
httpcloak_session_idle_time: nativeLibHandle.func("httpcloak_session_idle_time", "int64", ["int64"]),
|
|
888
|
+
httpcloak_session_is_active: nativeLibHandle.func("httpcloak_session_is_active", "int", ["int64"]),
|
|
889
|
+
httpcloak_session_touch: nativeLibHandle.func("httpcloak_session_touch", "void", ["int64"]),
|
|
890
|
+
httpcloak_session_set_conditional_cache: nativeLibHandle.func("httpcloak_session_set_conditional_cache", "void", ["int64", "int"]),
|
|
891
|
+
httpcloak_session_get_conditional_cache: nativeLibHandle.func("httpcloak_session_get_conditional_cache", "int", ["int64"]),
|
|
892
|
+
httpcloak_session_set_client_hints: nativeLibHandle.func("httpcloak_session_set_client_hints", "void", ["int64", "int"]),
|
|
893
|
+
httpcloak_session_get_client_hints: nativeLibHandle.func("httpcloak_session_get_client_hints", "int", ["int64"]),
|
|
894
|
+
httpcloak_session_set_high_entropy_client_hints: nativeLibHandle.func("httpcloak_session_set_high_entropy_client_hints", "void", ["int64", "int"]),
|
|
895
|
+
httpcloak_session_get_high_entropy_client_hints: nativeLibHandle.func("httpcloak_session_get_high_entropy_client_hints", "int", ["int64"]),
|
|
896
|
+
httpcloak_session_set_follow_redirects: nativeLibHandle.func("httpcloak_session_set_follow_redirects", "void", ["int64", "int"]),
|
|
897
|
+
httpcloak_session_get_follow_redirects: nativeLibHandle.func("httpcloak_session_get_follow_redirects", "int", ["int64"]),
|
|
898
|
+
httpcloak_session_set_max_redirects: nativeLibHandle.func("httpcloak_session_set_max_redirects", "void", ["int64", "int"]),
|
|
899
|
+
httpcloak_session_get_max_redirects: nativeLibHandle.func("httpcloak_session_get_max_redirects", "int", ["int64"]),
|
|
900
|
+
httpcloak_session_set_identifier: nativeLibHandle.func("httpcloak_session_set_identifier", "void", ["int64", "str"]),
|
|
833
901
|
httpcloak_free_string: httpcloakFreeString,
|
|
834
902
|
httpcloak_version: nativeLibHandle.func("httpcloak_version", HeapStr, []),
|
|
835
903
|
httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", HeapStr, []),
|
|
@@ -841,6 +909,7 @@ function getLib() {
|
|
|
841
909
|
httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
|
|
842
910
|
httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
|
|
843
911
|
httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
|
|
912
|
+
httpcloak_cancel_request: nativeLibHandle.func("httpcloak_cancel_request", "void", ["int64"]),
|
|
844
913
|
// Streaming functions
|
|
845
914
|
httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
|
|
846
915
|
httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
|
|
@@ -848,6 +917,15 @@ function getLib() {
|
|
|
848
917
|
httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", HeapStr, ["int64"]),
|
|
849
918
|
httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", HeapStr, ["int64", "int64"]),
|
|
850
919
|
httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
|
|
920
|
+
// Chunked upload state machine (mirrors Python's stream_upload). Lets
|
|
921
|
+
// bindings ship arbitrarily-large bodies without buffering in memory:
|
|
922
|
+
// upload_start opens a pipe to the Go side, upload_write_raw streams
|
|
923
|
+
// chunks straight to the wire, upload_finish reads the response,
|
|
924
|
+
// upload_cancel tears down a partial upload on error.
|
|
925
|
+
httpcloak_upload_start: nativeLibHandle.func("httpcloak_upload_start", "int64", ["int64", "str", "str"]),
|
|
926
|
+
httpcloak_upload_write_raw: nativeLibHandle.func("httpcloak_upload_write_raw", "int", ["int64", "void*", "int"]),
|
|
927
|
+
httpcloak_upload_finish: nativeLibHandle.func("httpcloak_upload_finish", HeapStr, ["int64"]),
|
|
928
|
+
httpcloak_upload_cancel: nativeLibHandle.func("httpcloak_upload_cancel", "void", ["int64"]),
|
|
851
929
|
// Raw response functions for fast-path (zero-copy)
|
|
852
930
|
httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
|
|
853
931
|
httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
|
|
@@ -881,6 +959,8 @@ function getLib() {
|
|
|
881
959
|
httpcloak_local_proxy_get_stats: nativeLibHandle.func("httpcloak_local_proxy_get_stats", HeapStr, ["int64"]),
|
|
882
960
|
httpcloak_local_proxy_register_session: nativeLibHandle.func("httpcloak_local_proxy_register_session", HeapStr, ["int64", "str", "int64"]),
|
|
883
961
|
httpcloak_local_proxy_unregister_session: nativeLibHandle.func("httpcloak_local_proxy_unregister_session", "int", ["int64", "str"]),
|
|
962
|
+
httpcloak_local_proxy_list_sessions: nativeLibHandle.func("httpcloak_local_proxy_list_sessions", HeapStr, ["int64"]),
|
|
963
|
+
httpcloak_local_proxy_has_session: nativeLibHandle.func("httpcloak_local_proxy_has_session", "int", ["int64", "str"]),
|
|
884
964
|
// Session cache callbacks
|
|
885
965
|
httpcloak_set_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_session_cache_callbacks", "void", [
|
|
886
966
|
koffi.pointer(SessionCacheGetProto),
|
|
@@ -1010,7 +1090,7 @@ class AsyncCallbackManager {
|
|
|
1010
1090
|
* Register a new async request
|
|
1011
1091
|
* @returns {{ callbackId: number, promise: Promise<Response> }}
|
|
1012
1092
|
*/
|
|
1013
|
-
registerRequest(nativeLib) {
|
|
1093
|
+
registerRequest(nativeLib, signal) {
|
|
1014
1094
|
this._ensureCallback();
|
|
1015
1095
|
|
|
1016
1096
|
// Register callback with Go (each request gets unique ID)
|
|
@@ -1027,6 +1107,43 @@ class AsyncCallbackManager {
|
|
|
1027
1107
|
this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
|
|
1028
1108
|
this._ref(); // Keep event loop alive
|
|
1029
1109
|
|
|
1110
|
+
// AbortSignal integration. If the caller supplies a signal that is
|
|
1111
|
+
// already aborted, cancel synchronously; otherwise install a listener
|
|
1112
|
+
// that fires once. Pre-aborted signals never deliver the "abort" event,
|
|
1113
|
+
// so the explicit early-path matters.
|
|
1114
|
+
//
|
|
1115
|
+
// Order matters in onAbort:
|
|
1116
|
+
// 1) cancel_request → unblocks the Go goroutine (ctx.Done fires)
|
|
1117
|
+
// 2) unregister_callback → removes the entry from Go's asyncCallbacks
|
|
1118
|
+
// map so the goroutine's final invokeCallback() finds !exists and
|
|
1119
|
+
// returns silently. Without this, Go would still relay an error
|
|
1120
|
+
// callback through koffi after the JS side has already settled
|
|
1121
|
+
// and torn down its callback context, which crashes node with
|
|
1122
|
+
// "Error::ThrowAsJavaScriptException napi_throw" during exit.
|
|
1123
|
+
if (signal && typeof signal === "object" && "aborted" in signal) {
|
|
1124
|
+
const onAbort = () => {
|
|
1125
|
+
const cid = Number(callbackId);
|
|
1126
|
+
try {
|
|
1127
|
+
nativeLib.httpcloak_cancel_request(cid);
|
|
1128
|
+
nativeLib.httpcloak_unregister_callback(cid);
|
|
1129
|
+
} catch {
|
|
1130
|
+
// Best-effort: the request may have already settled.
|
|
1131
|
+
}
|
|
1132
|
+
const pending = this._pendingRequests.get(cid);
|
|
1133
|
+
if (pending) {
|
|
1134
|
+
this._pendingRequests.delete(cid);
|
|
1135
|
+
this._unref();
|
|
1136
|
+
const reason = signal.reason ?? new Error("AbortError");
|
|
1137
|
+
pending.reject(reason);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
if (signal.aborted) {
|
|
1141
|
+
onAbort();
|
|
1142
|
+
} else if (typeof signal.addEventListener === "function") {
|
|
1143
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1030
1147
|
return { callbackId, promise };
|
|
1031
1148
|
}
|
|
1032
1149
|
}
|
|
@@ -1362,6 +1479,11 @@ class Session {
|
|
|
1362
1479
|
enableSpeculativeTls = false,
|
|
1363
1480
|
switchProtocol = null,
|
|
1364
1481
|
withoutCookieJar = false,
|
|
1482
|
+
withoutConditionalCache = false,
|
|
1483
|
+
withoutClientHints = false,
|
|
1484
|
+
withoutHighEntropyClientHints = false,
|
|
1485
|
+
disableEch = false,
|
|
1486
|
+
disableHttp3 = false,
|
|
1365
1487
|
ja3 = null,
|
|
1366
1488
|
akamai = null,
|
|
1367
1489
|
extraFp = null,
|
|
@@ -1439,6 +1561,23 @@ class Session {
|
|
|
1439
1561
|
if (withoutCookieJar) {
|
|
1440
1562
|
config.without_cookie_jar = true;
|
|
1441
1563
|
}
|
|
1564
|
+
if (withoutConditionalCache) {
|
|
1565
|
+
config.without_conditional_cache = true;
|
|
1566
|
+
}
|
|
1567
|
+
// Full strip: drop ALL sec-ch-ua client hints, including the always-on trio.
|
|
1568
|
+
if (withoutClientHints) {
|
|
1569
|
+
config.without_client_hints = true;
|
|
1570
|
+
}
|
|
1571
|
+
// High-entropy strip: keep the always-on trio, drop only the Accept-CH hints.
|
|
1572
|
+
if (withoutHighEntropyClientHints) {
|
|
1573
|
+
config.without_high_entropy_client_hints = true;
|
|
1574
|
+
}
|
|
1575
|
+
if (disableEch) {
|
|
1576
|
+
config.disable_ech = true;
|
|
1577
|
+
}
|
|
1578
|
+
if (disableHttp3) {
|
|
1579
|
+
config.disable_http3 = true;
|
|
1580
|
+
}
|
|
1442
1581
|
if (ja3) {
|
|
1443
1582
|
config.ja3 = ja3;
|
|
1444
1583
|
}
|
|
@@ -1602,7 +1741,7 @@ class Session {
|
|
|
1602
1741
|
* @returns {Response} Response object
|
|
1603
1742
|
*/
|
|
1604
1743
|
getSync(url, options = {}) {
|
|
1605
|
-
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1744
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
|
|
1606
1745
|
|
|
1607
1746
|
url = addParamsToUrl(url, params);
|
|
1608
1747
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1619,6 +1758,18 @@ class Session {
|
|
|
1619
1758
|
if (fetchMode) {
|
|
1620
1759
|
reqOptions.fetch_mode = fetchMode;
|
|
1621
1760
|
}
|
|
1761
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1762
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1763
|
+
}
|
|
1764
|
+
if (disableConditionalCache) {
|
|
1765
|
+
reqOptions.disable_conditional_cache = true;
|
|
1766
|
+
}
|
|
1767
|
+
if (disableClientHints) {
|
|
1768
|
+
reqOptions.disable_client_hints = true;
|
|
1769
|
+
}
|
|
1770
|
+
if (disableHighEntropyClientHints) {
|
|
1771
|
+
reqOptions.disable_high_entropy_client_hints = true;
|
|
1772
|
+
}
|
|
1622
1773
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1623
1774
|
|
|
1624
1775
|
const startTime = Date.now();
|
|
@@ -1648,7 +1799,7 @@ class Session {
|
|
|
1648
1799
|
* @returns {Response} Response object
|
|
1649
1800
|
*/
|
|
1650
1801
|
postSync(url, options = {}) {
|
|
1651
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null } = options;
|
|
1802
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
|
|
1652
1803
|
|
|
1653
1804
|
url = addParamsToUrl(url, params);
|
|
1654
1805
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1693,6 +1844,18 @@ class Session {
|
|
|
1693
1844
|
if (fetchMode) {
|
|
1694
1845
|
reqOptions.fetch_mode = fetchMode;
|
|
1695
1846
|
}
|
|
1847
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1848
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1849
|
+
}
|
|
1850
|
+
if (disableConditionalCache) {
|
|
1851
|
+
reqOptions.disable_conditional_cache = true;
|
|
1852
|
+
}
|
|
1853
|
+
if (disableClientHints) {
|
|
1854
|
+
reqOptions.disable_client_hints = true;
|
|
1855
|
+
}
|
|
1856
|
+
if (disableHighEntropyClientHints) {
|
|
1857
|
+
reqOptions.disable_high_entropy_client_hints = true;
|
|
1858
|
+
}
|
|
1696
1859
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1697
1860
|
|
|
1698
1861
|
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
@@ -1717,7 +1880,7 @@ class Session {
|
|
|
1717
1880
|
* @returns {Response} Response object
|
|
1718
1881
|
*/
|
|
1719
1882
|
requestSync(method, url, options = {}) {
|
|
1720
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
1883
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
|
|
1721
1884
|
|
|
1722
1885
|
url = addParamsToUrl(url, params);
|
|
1723
1886
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1760,6 +1923,10 @@ class Session {
|
|
|
1760
1923
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1761
1924
|
if (timeout) requestConfig.timeout = timeout;
|
|
1762
1925
|
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
1926
|
+
if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
|
|
1927
|
+
if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
|
|
1928
|
+
if (disableClientHints) requestConfig.disable_client_hints = true;
|
|
1929
|
+
if (disableHighEntropyClientHints) requestConfig.disable_high_entropy_client_hints = true;
|
|
1763
1930
|
|
|
1764
1931
|
const bodyPtr = bodyBuffer || Buffer.alloc(0);
|
|
1765
1932
|
const bodyLen = bodyBuffer ? bodyBuffer.length : 0;
|
|
@@ -1790,7 +1957,7 @@ class Session {
|
|
|
1790
1957
|
* @returns {Promise<Response>} Response object
|
|
1791
1958
|
*/
|
|
1792
1959
|
get(url, options = {}) {
|
|
1793
|
-
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
1960
|
+
const { headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false, signal = null } = options;
|
|
1794
1961
|
|
|
1795
1962
|
url = addParamsToUrl(url, params);
|
|
1796
1963
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -1813,11 +1980,23 @@ class Session {
|
|
|
1813
1980
|
// httpcloak_request_async — that path also uses time.Second).
|
|
1814
1981
|
reqOptions.timeout = timeout;
|
|
1815
1982
|
}
|
|
1983
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
1984
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
1985
|
+
}
|
|
1986
|
+
if (disableConditionalCache) {
|
|
1987
|
+
reqOptions.disable_conditional_cache = true;
|
|
1988
|
+
}
|
|
1989
|
+
if (disableClientHints) {
|
|
1990
|
+
reqOptions.disable_client_hints = true;
|
|
1991
|
+
}
|
|
1992
|
+
if (disableHighEntropyClientHints) {
|
|
1993
|
+
reqOptions.disable_high_entropy_client_hints = true;
|
|
1994
|
+
}
|
|
1816
1995
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1817
1996
|
|
|
1818
1997
|
// Register async request with callback manager
|
|
1819
1998
|
const manager = getAsyncManager();
|
|
1820
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1999
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1821
2000
|
|
|
1822
2001
|
// Start async request
|
|
1823
2002
|
this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
|
|
@@ -1833,16 +2012,23 @@ class Session {
|
|
|
1833
2012
|
* @returns {Promise<Response>} Response object
|
|
1834
2013
|
*/
|
|
1835
2014
|
post(url, options = {}) {
|
|
1836
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null } = options;
|
|
2015
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, fetchMode = null, timeout = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false, signal = null } = options;
|
|
1837
2016
|
|
|
1838
2017
|
url = addParamsToUrl(url, params);
|
|
1839
2018
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1840
2019
|
|
|
2020
|
+
// bodyEncoding tells clib whether `body` is plain text or base64-encoded
|
|
2021
|
+
// binary. Default "" = text. Anything that carries arbitrary bytes
|
|
2022
|
+
// (Buffer, multipart) flows through as base64 so NUL bytes don't truncate
|
|
2023
|
+
// the C string at the cgo boundary.
|
|
2024
|
+
let bodyEncoding = "";
|
|
2025
|
+
|
|
1841
2026
|
// Handle multipart file upload
|
|
1842
2027
|
if (files !== null) {
|
|
1843
2028
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1844
2029
|
const multipart = encodeMultipart(formData, files);
|
|
1845
|
-
body = multipart.body.toString("
|
|
2030
|
+
body = multipart.body.toString("base64");
|
|
2031
|
+
bodyEncoding = "base64";
|
|
1846
2032
|
mergedHeaders = mergedHeaders || {};
|
|
1847
2033
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1848
2034
|
}
|
|
@@ -1862,9 +2048,11 @@ class Session {
|
|
|
1862
2048
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1863
2049
|
}
|
|
1864
2050
|
}
|
|
1865
|
-
// Handle Buffer body
|
|
2051
|
+
// Handle Buffer body (binary payload). base64-encode so NUL bytes and
|
|
2052
|
+
// non-UTF-8 sequences survive the cgo C-string round-trip intact.
|
|
1866
2053
|
else if (Buffer.isBuffer(body)) {
|
|
1867
|
-
body = body.toString("
|
|
2054
|
+
body = body.toString("base64");
|
|
2055
|
+
bodyEncoding = "base64";
|
|
1868
2056
|
}
|
|
1869
2057
|
|
|
1870
2058
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1884,11 +2072,26 @@ class Session {
|
|
|
1884
2072
|
// Public API: seconds. clib post_async path enforces in seconds.
|
|
1885
2073
|
reqOptions.timeout = timeout;
|
|
1886
2074
|
}
|
|
2075
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2076
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2077
|
+
}
|
|
2078
|
+
if (disableConditionalCache) {
|
|
2079
|
+
reqOptions.disable_conditional_cache = true;
|
|
2080
|
+
}
|
|
2081
|
+
if (disableClientHints) {
|
|
2082
|
+
reqOptions.disable_client_hints = true;
|
|
2083
|
+
}
|
|
2084
|
+
if (disableHighEntropyClientHints) {
|
|
2085
|
+
reqOptions.disable_high_entropy_client_hints = true;
|
|
2086
|
+
}
|
|
2087
|
+
if (bodyEncoding) {
|
|
2088
|
+
reqOptions.body_encoding = bodyEncoding;
|
|
2089
|
+
}
|
|
1887
2090
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
1888
2091
|
|
|
1889
2092
|
// Register async request with callback manager
|
|
1890
2093
|
const manager = getAsyncManager();
|
|
1891
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
2094
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1892
2095
|
|
|
1893
2096
|
// Start async request
|
|
1894
2097
|
this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
|
|
@@ -1905,16 +2108,22 @@ class Session {
|
|
|
1905
2108
|
* @returns {Promise<Response>} Response object
|
|
1906
2109
|
*/
|
|
1907
2110
|
request(method, url, options = {}) {
|
|
1908
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null } = options;
|
|
2111
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null, fetchMode = null, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false, signal = null } = options;
|
|
1909
2112
|
|
|
1910
2113
|
url = addParamsToUrl(url, params);
|
|
1911
2114
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
1912
2115
|
|
|
2116
|
+
// bodyEncoding tells clib whether `body` is text or base64 binary.
|
|
2117
|
+
// Binary payloads (Buffer / multipart) flow through as base64 so they
|
|
2118
|
+
// survive JSON serialization + the cgo C-string boundary intact.
|
|
2119
|
+
let bodyEncoding = "";
|
|
2120
|
+
|
|
1913
2121
|
// Handle multipart file upload
|
|
1914
2122
|
if (files !== null) {
|
|
1915
2123
|
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1916
2124
|
const multipart = encodeMultipart(formData, files);
|
|
1917
|
-
body = multipart.body.toString("
|
|
2125
|
+
body = multipart.body.toString("base64");
|
|
2126
|
+
bodyEncoding = "base64";
|
|
1918
2127
|
mergedHeaders = mergedHeaders || {};
|
|
1919
2128
|
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1920
2129
|
}
|
|
@@ -1934,9 +2143,11 @@ class Session {
|
|
|
1934
2143
|
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1935
2144
|
}
|
|
1936
2145
|
}
|
|
1937
|
-
// Handle Buffer body
|
|
2146
|
+
// Handle Buffer body (binary payload). base64-encode so non-UTF-8 bytes
|
|
2147
|
+
// and NUL bytes survive JSON.stringify + the C boundary.
|
|
1938
2148
|
else if (Buffer.isBuffer(body)) {
|
|
1939
|
-
body = body.toString("
|
|
2149
|
+
body = body.toString("base64");
|
|
2150
|
+
bodyEncoding = "base64";
|
|
1940
2151
|
}
|
|
1941
2152
|
|
|
1942
2153
|
// Use request auth if provided, otherwise fall back to session auth
|
|
@@ -1950,12 +2161,17 @@ class Session {
|
|
|
1950
2161
|
};
|
|
1951
2162
|
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1952
2163
|
if (body) requestConfig.body = body;
|
|
2164
|
+
if (bodyEncoding) requestConfig.body_encoding = bodyEncoding;
|
|
1953
2165
|
if (timeout) requestConfig.timeout = timeout;
|
|
1954
2166
|
if (fetchMode) requestConfig.fetch_mode = fetchMode;
|
|
2167
|
+
if (allowRedirects !== null && allowRedirects !== undefined) requestConfig.follow_redirects = !!allowRedirects;
|
|
2168
|
+
if (disableConditionalCache) requestConfig.disable_conditional_cache = true;
|
|
2169
|
+
if (disableClientHints) requestConfig.disable_client_hints = true;
|
|
2170
|
+
if (disableHighEntropyClientHints) requestConfig.disable_high_entropy_client_hints = true;
|
|
1955
2171
|
|
|
1956
2172
|
// Register async request with callback manager
|
|
1957
2173
|
const manager = getAsyncManager();
|
|
1958
|
-
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
2174
|
+
const { callbackId, promise } = manager.registerRequest(this._lib, signal);
|
|
1959
2175
|
|
|
1960
2176
|
// Start async request
|
|
1961
2177
|
this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
|
|
@@ -2103,6 +2319,160 @@ class Session {
|
|
|
2103
2319
|
return this.getCookies();
|
|
2104
2320
|
}
|
|
2105
2321
|
|
|
2322
|
+
// ===========================================================================
|
|
2323
|
+
// Conditional Cache and Redirect Runtime Control
|
|
2324
|
+
// ===========================================================================
|
|
2325
|
+
|
|
2326
|
+
/**
|
|
2327
|
+
* Drop the session's per-URL conditional-cache map (ETag / Last-Modified).
|
|
2328
|
+
* The next request to each URL goes out without If-None-Match /
|
|
2329
|
+
* If-Modified-Since headers. Cookies and TLS tickets are not touched.
|
|
2330
|
+
*/
|
|
2331
|
+
clearCache() {
|
|
2332
|
+
this._lib.httpcloak_session_clear_cache(this._handle);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* Return a snapshot of session counters, timestamps and transport-level
|
|
2337
|
+
* metrics. Useful for long-running scrapers that want per-session metrics
|
|
2338
|
+
* scraped into Prometheus / Datadog / etc.
|
|
2339
|
+
*
|
|
2340
|
+
* Keys: id, preset, created_at (Unix ns), last_used (Unix ns),
|
|
2341
|
+
* request_count, active, cookie_count, cache_entry_count, age_ns,
|
|
2342
|
+
* idle_time_ns, transport_stats (per-protocol object).
|
|
2343
|
+
*
|
|
2344
|
+
* @returns {Object} Stats snapshot, or empty object if the session is closed.
|
|
2345
|
+
*/
|
|
2346
|
+
stats() {
|
|
2347
|
+
const ptr = this._lib.httpcloak_session_stats(this._handle);
|
|
2348
|
+
const raw = resultToString(ptr);
|
|
2349
|
+
if (!raw) return {};
|
|
2350
|
+
try { return JSON.parse(raw); } catch { return {}; }
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/**
|
|
2354
|
+
* Return the time since the session last serviced a request, in seconds.
|
|
2355
|
+
* Returns -1 if the session handle is invalid.
|
|
2356
|
+
*
|
|
2357
|
+
* @returns {number} Idle time in seconds (may be fractional).
|
|
2358
|
+
*/
|
|
2359
|
+
idleTime() {
|
|
2360
|
+
const ns = Number(this._lib.httpcloak_session_idle_time(this._handle));
|
|
2361
|
+
if (ns < 0) return -1;
|
|
2362
|
+
return ns / 1_000_000_000;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* Return true if the session is still usable (close() has not been called
|
|
2367
|
+
* and the handle is valid).
|
|
2368
|
+
*
|
|
2369
|
+
* @returns {boolean}
|
|
2370
|
+
*/
|
|
2371
|
+
isActive() {
|
|
2372
|
+
return this._lib.httpcloak_session_is_active(this._handle) === 1;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/**
|
|
2376
|
+
* Reset the idle timer to now without issuing a request. Useful in
|
|
2377
|
+
* long-running pools where an external heartbeat shouldn't let a session
|
|
2378
|
+
* look idle to a reaper.
|
|
2379
|
+
*/
|
|
2380
|
+
touch() {
|
|
2381
|
+
this._lib.httpcloak_session_touch(this._handle);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/**
|
|
2385
|
+
* Toggle the session's ETag / If-Modified-Since handling at runtime.
|
|
2386
|
+
* When disabled, the session stops injecting cache validators on outgoing
|
|
2387
|
+
* requests and stops storing them from responses; the existing cache map
|
|
2388
|
+
* is preserved (re-enabling resumes using it). Pair with clearCache() to
|
|
2389
|
+
* also wipe previously-stored validators.
|
|
2390
|
+
* @param {boolean} enabled
|
|
2391
|
+
*/
|
|
2392
|
+
setConditionalCache(enabled) {
|
|
2393
|
+
this._lib.httpcloak_session_set_conditional_cache(this._handle, enabled ? 1 : 0);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* Return the session's current conditional-cache state.
|
|
2398
|
+
* @returns {boolean}
|
|
2399
|
+
*/
|
|
2400
|
+
getConditionalCache() {
|
|
2401
|
+
return this._lib.httpcloak_session_get_conditional_cache(this._handle) !== 0;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Toggle the session's client-hints handling at runtime (full strip).
|
|
2406
|
+
* When disabled, the session drops ALL sec-ch-ua client hints, including
|
|
2407
|
+
* the always-on trio (sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform).
|
|
2408
|
+
* The change takes effect on the next request and persists until set again.
|
|
2409
|
+
* @param {boolean} enabled
|
|
2410
|
+
*/
|
|
2411
|
+
setClientHints(enabled) {
|
|
2412
|
+
this._lib.httpcloak_session_set_client_hints(this._handle, enabled ? 1 : 0);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
/**
|
|
2416
|
+
* Return the session's current client-hints state.
|
|
2417
|
+
* @returns {boolean}
|
|
2418
|
+
*/
|
|
2419
|
+
getClientHints() {
|
|
2420
|
+
return this._lib.httpcloak_session_get_client_hints(this._handle) !== 0;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
/**
|
|
2424
|
+
* Toggle the session's high-entropy client-hints handling at runtime.
|
|
2425
|
+
* When disabled, the session keeps the always-on trio but drops only the
|
|
2426
|
+
* high-entropy Accept-CH hints (e.g. sec-ch-ua-platform-version, -arch,
|
|
2427
|
+
* -model, -bitness, -full-version-list). Takes effect on the next request.
|
|
2428
|
+
* @param {boolean} enabled
|
|
2429
|
+
*/
|
|
2430
|
+
setHighEntropyClientHints(enabled) {
|
|
2431
|
+
this._lib.httpcloak_session_set_high_entropy_client_hints(this._handle, enabled ? 1 : 0);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
/**
|
|
2435
|
+
* Return the session's current high-entropy client-hints state.
|
|
2436
|
+
* @returns {boolean}
|
|
2437
|
+
*/
|
|
2438
|
+
getHighEntropyClientHints() {
|
|
2439
|
+
return this._lib.httpcloak_session_get_high_entropy_client_hints(this._handle) !== 0;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Toggle the session's redirect-following policy at runtime. The change
|
|
2444
|
+
* takes effect on the next request and persists until set again.
|
|
2445
|
+
* @param {boolean} enabled
|
|
2446
|
+
*/
|
|
2447
|
+
setFollowRedirects(enabled) {
|
|
2448
|
+
this._lib.httpcloak_session_set_follow_redirects(this._handle, enabled ? 1 : 0);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/**
|
|
2452
|
+
* Return the session's current redirect-following policy.
|
|
2453
|
+
* @returns {boolean}
|
|
2454
|
+
*/
|
|
2455
|
+
getFollowRedirects() {
|
|
2456
|
+
return this._lib.httpcloak_session_get_follow_redirects(this._handle) !== 0;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Update the session's redirect cap at runtime. Values of zero or below
|
|
2461
|
+
* are ignored, leaving the prior cap (or the default of 10) in place.
|
|
2462
|
+
* @param {number} max
|
|
2463
|
+
*/
|
|
2464
|
+
setMaxRedirects(max) {
|
|
2465
|
+
this._lib.httpcloak_session_set_max_redirects(this._handle, max);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/**
|
|
2469
|
+
* Return the session's current redirect cap.
|
|
2470
|
+
* @returns {number}
|
|
2471
|
+
*/
|
|
2472
|
+
getMaxRedirects() {
|
|
2473
|
+
return this._lib.httpcloak_session_get_max_redirects(this._handle);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2106
2476
|
// ===========================================================================
|
|
2107
2477
|
// Proxy Management
|
|
2108
2478
|
// ===========================================================================
|
|
@@ -2399,7 +2769,7 @@ class Session {
|
|
|
2399
2769
|
* stream.close();
|
|
2400
2770
|
*/
|
|
2401
2771
|
getStream(url, options = {}) {
|
|
2402
|
-
const { params, headers, cookies, timeout } = options;
|
|
2772
|
+
const { params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
|
|
2403
2773
|
|
|
2404
2774
|
// Add params to URL
|
|
2405
2775
|
if (params) {
|
|
@@ -2426,6 +2796,18 @@ class Session {
|
|
|
2426
2796
|
if (timeout) {
|
|
2427
2797
|
reqOptions.timeout = timeout;
|
|
2428
2798
|
}
|
|
2799
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2800
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2801
|
+
}
|
|
2802
|
+
if (disableConditionalCache) {
|
|
2803
|
+
reqOptions.disable_conditional_cache = true;
|
|
2804
|
+
}
|
|
2805
|
+
if (disableClientHints) {
|
|
2806
|
+
reqOptions.disable_client_hints = true;
|
|
2807
|
+
}
|
|
2808
|
+
if (disableHighEntropyClientHints) {
|
|
2809
|
+
reqOptions.disable_high_entropy_client_hints = true;
|
|
2810
|
+
}
|
|
2429
2811
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2430
2812
|
|
|
2431
2813
|
// Start stream
|
|
@@ -2465,7 +2847,7 @@ class Session {
|
|
|
2465
2847
|
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
2466
2848
|
*/
|
|
2467
2849
|
postStream(url, options = {}) {
|
|
2468
|
-
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
|
|
2850
|
+
const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
|
|
2469
2851
|
|
|
2470
2852
|
// Add params to URL
|
|
2471
2853
|
if (params) {
|
|
@@ -2505,6 +2887,18 @@ class Session {
|
|
|
2505
2887
|
if (timeout) {
|
|
2506
2888
|
reqOptions.timeout = timeout;
|
|
2507
2889
|
}
|
|
2890
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2891
|
+
reqOptions.follow_redirects = !!allowRedirects;
|
|
2892
|
+
}
|
|
2893
|
+
if (disableConditionalCache) {
|
|
2894
|
+
reqOptions.disable_conditional_cache = true;
|
|
2895
|
+
}
|
|
2896
|
+
if (disableClientHints) {
|
|
2897
|
+
reqOptions.disable_client_hints = true;
|
|
2898
|
+
}
|
|
2899
|
+
if (disableHighEntropyClientHints) {
|
|
2900
|
+
reqOptions.disable_high_entropy_client_hints = true;
|
|
2901
|
+
}
|
|
2508
2902
|
const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
|
|
2509
2903
|
|
|
2510
2904
|
// Start stream
|
|
@@ -2543,7 +2937,7 @@ class Session {
|
|
|
2543
2937
|
* @returns {StreamResponse} - Streaming response for chunked reading
|
|
2544
2938
|
*/
|
|
2545
2939
|
requestStream(method, url, options = {}) {
|
|
2546
|
-
const { body, params, headers, cookies, timeout } = options;
|
|
2940
|
+
const { body, params, headers, cookies, timeout, allowRedirects = null, disableConditionalCache = false, disableClientHints = false, disableHighEntropyClientHints = false } = options;
|
|
2547
2941
|
|
|
2548
2942
|
// Add params to URL
|
|
2549
2943
|
if (params) {
|
|
@@ -2576,6 +2970,18 @@ class Session {
|
|
|
2576
2970
|
if (timeout) {
|
|
2577
2971
|
requestConfig.timeout = timeout;
|
|
2578
2972
|
}
|
|
2973
|
+
if (allowRedirects !== null && allowRedirects !== undefined) {
|
|
2974
|
+
requestConfig.follow_redirects = !!allowRedirects;
|
|
2975
|
+
}
|
|
2976
|
+
if (disableConditionalCache) {
|
|
2977
|
+
requestConfig.disable_conditional_cache = true;
|
|
2978
|
+
}
|
|
2979
|
+
if (disableClientHints) {
|
|
2980
|
+
requestConfig.disable_client_hints = true;
|
|
2981
|
+
}
|
|
2982
|
+
if (disableHighEntropyClientHints) {
|
|
2983
|
+
requestConfig.disable_high_entropy_client_hints = true;
|
|
2984
|
+
}
|
|
2579
2985
|
|
|
2580
2986
|
// Start stream
|
|
2581
2987
|
const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
|
|
@@ -2894,6 +3300,84 @@ class Session {
|
|
|
2894
3300
|
patchFast(url, options = {}) {
|
|
2895
3301
|
return this.requestFast("PATCH", url, options);
|
|
2896
3302
|
}
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* Stream an arbitrary-sized body to the wire without buffering in memory.
|
|
3306
|
+
*
|
|
3307
|
+
* `chunks` is anything iterable that yields Buffer-shaped objects: an
|
|
3308
|
+
* AsyncIterable<Buffer> (e.g. an `fs.createReadStream(path)`), an
|
|
3309
|
+
* Iterable<Buffer> (e.g. an array), or a sync generator that yields
|
|
3310
|
+
* Buffers. String chunks are auto-encoded as UTF-8.
|
|
3311
|
+
*
|
|
3312
|
+
* The Go side opens an `io.Pipe()` for the body when uploadStart returns;
|
|
3313
|
+
* each chunk is written straight through with no base64 / no JSON wrapping.
|
|
3314
|
+
* On error the partial upload is cancelled and the underlying connection
|
|
3315
|
+
* closed; on success uploadFinish reads the full response and parses it
|
|
3316
|
+
* into a regular `Response` (same shape as a normal post()).
|
|
3317
|
+
*
|
|
3318
|
+
* @param {string} method HTTP method (POST / PUT / PATCH typically)
|
|
3319
|
+
* @param {string} url
|
|
3320
|
+
* @param {AsyncIterable<Buffer>|Iterable<Buffer>} chunks Body chunks
|
|
3321
|
+
* @param {Object} [options]
|
|
3322
|
+
* @param {Object} [options.headers]
|
|
3323
|
+
* @param {string} [options.contentType="application/octet-stream"]
|
|
3324
|
+
* @param {number} [options.timeout] Per-request timeout in ms
|
|
3325
|
+
* @returns {Promise<Response>}
|
|
3326
|
+
*/
|
|
3327
|
+
async uploadStream(method, url, chunks, options = {}) {
|
|
3328
|
+
const { headers = null, contentType = "application/octet-stream", timeout = null } = options;
|
|
3329
|
+
const mergedHeaders = this._mergeHeaders(headers) || {};
|
|
3330
|
+
const optsJson = JSON.stringify({
|
|
3331
|
+
method: method.toUpperCase(),
|
|
3332
|
+
headers: mergedHeaders,
|
|
3333
|
+
content_type: contentType,
|
|
3334
|
+
...(timeout ? { timeout } : {}),
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3337
|
+
const uploadHandle = this._lib.httpcloak_upload_start(this._handle, url, optsJson);
|
|
3338
|
+
if (uploadHandle <= 0) {
|
|
3339
|
+
throw new HTTPCloakError("Failed to start streaming upload");
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
const startTime = Date.now();
|
|
3343
|
+
try {
|
|
3344
|
+
for await (const chunk of chunks) {
|
|
3345
|
+
const buf = Buffer.isBuffer(chunk)
|
|
3346
|
+
? chunk
|
|
3347
|
+
: typeof chunk === "string"
|
|
3348
|
+
? Buffer.from(chunk, "utf8")
|
|
3349
|
+
: Buffer.from(chunk);
|
|
3350
|
+
if (buf.length === 0) continue;
|
|
3351
|
+
const written = this._lib.httpcloak_upload_write_raw(uploadHandle, buf, buf.length);
|
|
3352
|
+
if (written < 0) {
|
|
3353
|
+
throw new HTTPCloakError("Failed to write upload chunk");
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
const resultPtr = this._lib.httpcloak_upload_finish(uploadHandle);
|
|
3358
|
+
const elapsed = Date.now() - startTime;
|
|
3359
|
+
const raw = resultToString(resultPtr);
|
|
3360
|
+
if (!raw) {
|
|
3361
|
+
throw new HTTPCloakError("Empty response from streaming upload");
|
|
3362
|
+
}
|
|
3363
|
+
const data = JSON.parse(raw);
|
|
3364
|
+
if (data.error) {
|
|
3365
|
+
throw new HTTPCloakError(data.error);
|
|
3366
|
+
}
|
|
3367
|
+
return new Response(data, elapsed);
|
|
3368
|
+
} catch (err) {
|
|
3369
|
+
try { this._lib.httpcloak_upload_cancel(uploadHandle); } catch { /* best-effort */ }
|
|
3370
|
+
throw err;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
/**
|
|
3375
|
+
* Convenience wrapper: streaming POST. Same semantics as
|
|
3376
|
+
* {@link uploadStream}('POST', ...).
|
|
3377
|
+
*/
|
|
3378
|
+
postUpload(url, chunks, options = {}) {
|
|
3379
|
+
return this.uploadStream("POST", url, chunks, options);
|
|
3380
|
+
}
|
|
2897
3381
|
}
|
|
2898
3382
|
|
|
2899
3383
|
// =============================================================================
|
|
@@ -3227,6 +3711,41 @@ class LocalProxy {
|
|
|
3227
3711
|
return result === 1;
|
|
3228
3712
|
}
|
|
3229
3713
|
|
|
3714
|
+
/**
|
|
3715
|
+
* Return the IDs of every session currently registered on this proxy.
|
|
3716
|
+
*
|
|
3717
|
+
* These are the same IDs the X-HTTPCloak-Session header accepts for
|
|
3718
|
+
* per-request session routing. Useful for sanity checks, GC of stale
|
|
3719
|
+
* registrations in long-running processes, and operational dashboards.
|
|
3720
|
+
*
|
|
3721
|
+
* @returns {string[]} List of registered session IDs (empty if none).
|
|
3722
|
+
*/
|
|
3723
|
+
listSessions() {
|
|
3724
|
+
const resultPtr = this._lib.httpcloak_local_proxy_list_sessions(this._handle);
|
|
3725
|
+
const raw = resultToString(resultPtr);
|
|
3726
|
+
if (!raw) return [];
|
|
3727
|
+
try {
|
|
3728
|
+
const parsed = JSON.parse(raw);
|
|
3729
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
3730
|
+
} catch {
|
|
3731
|
+
return [];
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
/**
|
|
3736
|
+
* Return true if a session with the given ID is currently registered.
|
|
3737
|
+
*
|
|
3738
|
+
* Cheaper than listSessions() + .includes() when callers only need an
|
|
3739
|
+
* existence check.
|
|
3740
|
+
*
|
|
3741
|
+
* @param {string} sessionId
|
|
3742
|
+
* @returns {boolean}
|
|
3743
|
+
*/
|
|
3744
|
+
hasSession(sessionId) {
|
|
3745
|
+
if (!sessionId) return false;
|
|
3746
|
+
return this._lib.httpcloak_local_proxy_has_session(this._handle, sessionId) === 1;
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3230
3749
|
/**
|
|
3231
3750
|
* Stop the local proxy server.
|
|
3232
3751
|
*/
|