httpcloak 1.5.9 → 1.6.0-beta.4
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 +4 -4
- package/lib/index.d.ts +123 -14
- package/lib/index.js +312 -53
- package/lib/index.mjs +21 -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-arm64/package.json +1 -1
- package/npm/win32-x64/package.json +1 -1
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -291,10 +291,10 @@ Connect to one server while requesting a different domain:
|
|
|
291
291
|
```javascript
|
|
292
292
|
const { Session } = require("httpcloak");
|
|
293
293
|
|
|
294
|
-
// Connect to
|
|
294
|
+
// Connect to example.com's IP but request www.cloudflare.com
|
|
295
295
|
const session = new Session({
|
|
296
296
|
preset: "chrome-143",
|
|
297
|
-
connectTo: { "www.cloudflare.com": "
|
|
297
|
+
connectTo: { "www.cloudflare.com": "example.com" },
|
|
298
298
|
});
|
|
299
299
|
|
|
300
300
|
const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
@@ -371,8 +371,8 @@ const session = new Session({
|
|
|
371
371
|
const { availablePresets } = require("httpcloak");
|
|
372
372
|
|
|
373
373
|
console.log(availablePresets());
|
|
374
|
-
// ['chrome-
|
|
375
|
-
// '
|
|
374
|
+
// ['chrome-144', 'chrome-143', 'chrome-141', 'chrome-133',
|
|
375
|
+
// 'firefox-133', 'safari-18', 'ios-chrome-144', ...]
|
|
376
376
|
```
|
|
377
377
|
|
|
378
378
|
## Response Object
|
package/lib/index.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export class Cookie {
|
|
|
11
11
|
name: string;
|
|
12
12
|
/** Cookie value */
|
|
13
13
|
value: string;
|
|
14
|
+
/** Cookie domain */
|
|
15
|
+
domain: string;
|
|
16
|
+
/** Cookie path */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Expiration date (RFC1123 format) */
|
|
19
|
+
expires: string;
|
|
20
|
+
/** Max age in seconds (0 means not set) */
|
|
21
|
+
maxAge: number;
|
|
22
|
+
/** Secure flag */
|
|
23
|
+
secure: boolean;
|
|
24
|
+
/** HttpOnly flag */
|
|
25
|
+
httpOnly: boolean;
|
|
26
|
+
/** SameSite attribute (Strict, Lax, None) */
|
|
27
|
+
sameSite: string;
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
export class RedirectInfo {
|
|
@@ -181,7 +195,7 @@ export class StreamResponse {
|
|
|
181
195
|
}
|
|
182
196
|
|
|
183
197
|
export interface SessionOptions {
|
|
184
|
-
/** Browser preset to use (default: "chrome-
|
|
198
|
+
/** Browser preset to use (default: "chrome-144") */
|
|
185
199
|
preset?: string;
|
|
186
200
|
/** Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port") */
|
|
187
201
|
proxy?: string;
|
|
@@ -219,6 +233,10 @@ export interface SessionOptions {
|
|
|
219
233
|
tlsOnly?: boolean;
|
|
220
234
|
/** QUIC idle timeout in seconds (default: 30). Set higher for long-lived HTTP/3 connections. */
|
|
221
235
|
quicIdleTimeout?: number;
|
|
236
|
+
/** Local IP address to bind outgoing connections (for IPv6 rotation with IP_FREEBIND on Linux) */
|
|
237
|
+
localAddress?: string;
|
|
238
|
+
/** Path to write TLS key log for Wireshark decryption (overrides SSLKEYLOGFILE env var) */
|
|
239
|
+
keyLogFile?: string;
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
export interface RequestOptions {
|
|
@@ -254,6 +272,12 @@ export class Session {
|
|
|
254
272
|
/** Close the session and release resources */
|
|
255
273
|
close(): void;
|
|
256
274
|
|
|
275
|
+
/** Refresh the session by closing all connections while keeping TLS session tickets.
|
|
276
|
+
* This simulates a browser page refresh - connections are severed but 0-RTT
|
|
277
|
+
* early data can be used on reconnection due to preserved session tickets.
|
|
278
|
+
*/
|
|
279
|
+
refresh(): void;
|
|
280
|
+
|
|
257
281
|
// Synchronous methods
|
|
258
282
|
/** Perform a synchronous GET request */
|
|
259
283
|
getSync(url: string, options?: RequestOptions): Response;
|
|
@@ -572,7 +596,7 @@ export class Session {
|
|
|
572
596
|
export interface LocalProxyOptions {
|
|
573
597
|
/** Port to listen on (default: 0 for auto-assign) */
|
|
574
598
|
port?: number;
|
|
575
|
-
/** Browser preset to use (default: "chrome-
|
|
599
|
+
/** Browser preset to use (default: "chrome-144") */
|
|
576
600
|
preset?: string;
|
|
577
601
|
/** Request timeout in seconds (default: 30) */
|
|
578
602
|
timeout?: number;
|
|
@@ -599,6 +623,38 @@ export interface LocalProxyStats {
|
|
|
599
623
|
bytesReceived: number;
|
|
600
624
|
}
|
|
601
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Local HTTP proxy server that forwards requests through httpcloak with TLS fingerprinting.
|
|
628
|
+
* Use this to transparently apply fingerprinting to any HTTP client (e.g., Undici, fetch).
|
|
629
|
+
*
|
|
630
|
+
* Supports per-request proxy rotation via X-Upstream-Proxy header.
|
|
631
|
+
* Supports per-request session routing via X-HTTPCloak-Session header.
|
|
632
|
+
*
|
|
633
|
+
* IMPORTANT: For distributed session caching to work with X-HTTPCloak-Session header,
|
|
634
|
+
* you MUST register the session with the proxy using registerSession() first.
|
|
635
|
+
* Without registration, cache callbacks will not be triggered for that session.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* // Basic usage
|
|
639
|
+
* const proxy = new LocalProxy({ preset: "chrome-144", tlsOnly: true });
|
|
640
|
+
* console.log(`Proxy running on ${proxy.proxyUrl}`);
|
|
641
|
+
* // Use with any HTTP client pointing to the proxy
|
|
642
|
+
* proxy.close();
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* // With distributed session cache
|
|
646
|
+
* const proxy = new LocalProxy({ port: 8888 });
|
|
647
|
+
* const session = new Session({ preset: 'chrome-144' });
|
|
648
|
+
*
|
|
649
|
+
* // Configure distributed cache
|
|
650
|
+
* httpcloak.configureSessionCache({
|
|
651
|
+
* get: async (key) => await redis.get(key),
|
|
652
|
+
* put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
|
|
653
|
+
* });
|
|
654
|
+
*
|
|
655
|
+
* // REQUIRED: Register session for cache callbacks to work
|
|
656
|
+
* proxy.registerSession('session-1', session);
|
|
657
|
+
*/
|
|
602
658
|
export class LocalProxy {
|
|
603
659
|
/**
|
|
604
660
|
* Create a new LocalProxy instance.
|
|
@@ -626,6 +682,9 @@ export class LocalProxy {
|
|
|
626
682
|
* Register a session with an ID for use with X-HTTPCloak-Session header.
|
|
627
683
|
* This allows per-request session routing through the proxy.
|
|
628
684
|
*
|
|
685
|
+
* IMPORTANT: This is REQUIRED for distributed session caching to work.
|
|
686
|
+
* Without registration, cache callbacks will not be triggered for the session.
|
|
687
|
+
*
|
|
629
688
|
* When a request is made through the proxy with the `X-HTTPCloak-Session: <sessionId>` header,
|
|
630
689
|
* the proxy will use the registered session for that request, applying its TLS fingerprint
|
|
631
690
|
* and cookies.
|
|
@@ -637,12 +696,13 @@ export class LocalProxy {
|
|
|
637
696
|
* @example
|
|
638
697
|
* ```typescript
|
|
639
698
|
* const proxy = new LocalProxy({ port: 8888 });
|
|
640
|
-
* const session = new Session({ preset: 'chrome-
|
|
699
|
+
* const session = new Session({ preset: 'chrome-144' });
|
|
641
700
|
*
|
|
642
|
-
* // Register session with ID
|
|
701
|
+
* // Register session with ID (required for cache callbacks)
|
|
643
702
|
* proxy.registerSession('user-1', session);
|
|
644
703
|
*
|
|
645
704
|
* // Now requests with X-HTTPCloak-Session: user-1 header will use this session
|
|
705
|
+
* // and trigger cache callbacks
|
|
646
706
|
* ```
|
|
647
707
|
*/
|
|
648
708
|
registerSession(sessionId: string, session: Session): void;
|
|
@@ -744,10 +804,13 @@ export const Preset: {
|
|
|
744
804
|
CHROME_131_LINUX: string;
|
|
745
805
|
CHROME_131_MACOS: string;
|
|
746
806
|
IOS_CHROME_143: string;
|
|
807
|
+
IOS_CHROME_144: string;
|
|
747
808
|
ANDROID_CHROME_143: string;
|
|
809
|
+
ANDROID_CHROME_144: string;
|
|
748
810
|
FIREFOX_133: string;
|
|
749
811
|
SAFARI_18: string;
|
|
750
812
|
IOS_SAFARI_17: string;
|
|
813
|
+
IOS_SAFARI_18: string;
|
|
751
814
|
all(): string[];
|
|
752
815
|
};
|
|
753
816
|
|
|
@@ -759,42 +822,48 @@ export interface SessionCacheOptions {
|
|
|
759
822
|
/**
|
|
760
823
|
* Function to get session data from cache.
|
|
761
824
|
* Returns JSON string with session data, or null if not found.
|
|
762
|
-
*
|
|
825
|
+
* Supports both sync and async callbacks.
|
|
763
826
|
*/
|
|
764
|
-
get?: (key: string) => string | null
|
|
827
|
+
get?: (key: string) => string | null | Promise<string | null>;
|
|
765
828
|
|
|
766
829
|
/**
|
|
767
830
|
* Function to store session data in cache.
|
|
768
831
|
* Returns 0 on success, non-zero on error.
|
|
769
|
-
*
|
|
832
|
+
* Supports both sync and async callbacks.
|
|
770
833
|
*/
|
|
771
|
-
put?: (key: string, value: string, ttlSeconds: number) => number
|
|
834
|
+
put?: (key: string, value: string, ttlSeconds: number) => number | Promise<number>;
|
|
772
835
|
|
|
773
836
|
/**
|
|
774
837
|
* Function to delete session data from cache.
|
|
775
838
|
* Returns 0 on success, non-zero on error.
|
|
776
|
-
*
|
|
839
|
+
* Supports both sync and async callbacks.
|
|
777
840
|
*/
|
|
778
|
-
delete?: (key: string) => number
|
|
841
|
+
delete?: (key: string) => number | Promise<number>;
|
|
779
842
|
|
|
780
843
|
/**
|
|
781
844
|
* Function to get ECH config from cache.
|
|
782
845
|
* Returns base64-encoded config, or null if not found.
|
|
783
|
-
*
|
|
846
|
+
* Supports both sync and async callbacks.
|
|
784
847
|
*/
|
|
785
|
-
getEch?: (key: string) => string | null
|
|
848
|
+
getEch?: (key: string) => string | null | Promise<string | null>;
|
|
786
849
|
|
|
787
850
|
/**
|
|
788
851
|
* Function to store ECH config in cache.
|
|
789
852
|
* Returns 0 on success, non-zero on error.
|
|
790
|
-
*
|
|
853
|
+
* Supports both sync and async callbacks.
|
|
791
854
|
*/
|
|
792
|
-
putEch?: (key: string, value: string, ttlSeconds: number) => number
|
|
855
|
+
putEch?: (key: string, value: string, ttlSeconds: number) => number | Promise<number>;
|
|
793
856
|
|
|
794
857
|
/**
|
|
795
858
|
* Error callback for cache operations.
|
|
796
859
|
*/
|
|
797
860
|
onError?: (operation: string, key: string, error: string) => void;
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Force async mode. If not specified, async mode is auto-detected
|
|
864
|
+
* based on whether any callback is an async function.
|
|
865
|
+
*/
|
|
866
|
+
async?: boolean;
|
|
798
867
|
}
|
|
799
868
|
|
|
800
869
|
/**
|
|
@@ -803,13 +872,42 @@ export interface SessionCacheOptions {
|
|
|
803
872
|
* Enables TLS session resumption across distributed httpcloak instances
|
|
804
873
|
* by storing session tickets in an external cache like Redis or Memcached.
|
|
805
874
|
*
|
|
875
|
+
* Supports both synchronous callbacks (for in-memory Map) and asynchronous
|
|
876
|
+
* callbacks (for Redis, database, etc.). Async mode is auto-detected when
|
|
877
|
+
* any callback is an async function.
|
|
878
|
+
*
|
|
806
879
|
* Cache key formats:
|
|
807
880
|
* - TLS sessions: httpcloak:sessions:{preset}:{protocol}:{host}:{port}
|
|
808
881
|
* - ECH configs: httpcloak:ech:{preset}:{host}:{port}
|
|
882
|
+
*
|
|
883
|
+
* @example
|
|
884
|
+
* // Sync example with Map
|
|
885
|
+
* const cache = new Map();
|
|
886
|
+
* const backend = new SessionCacheBackend({
|
|
887
|
+
* get: (key) => cache.get(key) || null,
|
|
888
|
+
* put: (key, value, ttl) => { cache.set(key, value); return 0; },
|
|
889
|
+
* delete: (key) => { cache.delete(key); return 0; },
|
|
890
|
+
* });
|
|
891
|
+
* backend.register();
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* // Async example with Redis
|
|
895
|
+
* const redis = new Redis();
|
|
896
|
+
* const backend = new SessionCacheBackend({
|
|
897
|
+
* get: async (key) => await redis.get(key),
|
|
898
|
+
* put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
|
|
899
|
+
* delete: async (key) => { await redis.del(key); return 0; },
|
|
900
|
+
* });
|
|
901
|
+
* backend.register();
|
|
809
902
|
*/
|
|
810
903
|
export class SessionCacheBackend {
|
|
811
904
|
constructor(options?: SessionCacheOptions);
|
|
812
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Check if this backend is running in async mode.
|
|
908
|
+
*/
|
|
909
|
+
readonly isAsync: boolean;
|
|
910
|
+
|
|
813
911
|
/**
|
|
814
912
|
* Register this cache backend globally.
|
|
815
913
|
* After registration, all new Session and LocalProxy instances will use
|
|
@@ -827,8 +925,19 @@ export class SessionCacheBackend {
|
|
|
827
925
|
/**
|
|
828
926
|
* Configure a distributed session cache backend.
|
|
829
927
|
*
|
|
928
|
+
* Supports both synchronous and asynchronous callbacks (auto-detected).
|
|
929
|
+
*
|
|
830
930
|
* @param options Cache configuration
|
|
831
931
|
* @returns The registered SessionCacheBackend instance
|
|
932
|
+
*
|
|
933
|
+
* @example
|
|
934
|
+
* // Using Redis with async callbacks
|
|
935
|
+
* const redis = new Redis();
|
|
936
|
+
* httpcloak.configureSessionCache({
|
|
937
|
+
* get: async (key) => await redis.get(key),
|
|
938
|
+
* put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
|
|
939
|
+
* delete: async (key) => { await redis.del(key); return 0; },
|
|
940
|
+
* });
|
|
832
941
|
*/
|
|
833
942
|
export function configureSessionCache(options: SessionCacheOptions): SessionCacheBackend;
|
|
834
943
|
|
package/lib/index.js
CHANGED
|
@@ -43,15 +43,11 @@ const Preset = {
|
|
|
43
43
|
// Chrome 133
|
|
44
44
|
CHROME_133: "chrome-133",
|
|
45
45
|
|
|
46
|
-
// Chrome 131
|
|
47
|
-
CHROME_131: "chrome-131",
|
|
48
|
-
CHROME_131_WINDOWS: "chrome-131-windows",
|
|
49
|
-
CHROME_131_LINUX: "chrome-131-linux",
|
|
50
|
-
CHROME_131_MACOS: "chrome-131-macos",
|
|
51
|
-
|
|
52
46
|
// Mobile Chrome
|
|
53
47
|
IOS_CHROME_143: "ios-chrome-143",
|
|
48
|
+
IOS_CHROME_144: "ios-chrome-144",
|
|
54
49
|
ANDROID_CHROME_143: "android-chrome-143",
|
|
50
|
+
ANDROID_CHROME_144: "android-chrome-144",
|
|
55
51
|
|
|
56
52
|
// Firefox
|
|
57
53
|
FIREFOX_133: "firefox-133",
|
|
@@ -59,6 +55,7 @@ const Preset = {
|
|
|
59
55
|
// Safari (desktop and mobile)
|
|
60
56
|
SAFARI_18: "safari-18",
|
|
61
57
|
IOS_SAFARI_17: "ios-safari-17",
|
|
58
|
+
IOS_SAFARI_18: "ios-safari-18",
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
61
|
* Get all available preset names
|
|
@@ -66,11 +63,12 @@ const Preset = {
|
|
|
66
63
|
*/
|
|
67
64
|
all() {
|
|
68
65
|
return [
|
|
66
|
+
this.CHROME_144, this.CHROME_144_WINDOWS, this.CHROME_144_LINUX, this.CHROME_144_MACOS,
|
|
69
67
|
this.CHROME_143, this.CHROME_143_WINDOWS, this.CHROME_143_LINUX, this.CHROME_143_MACOS,
|
|
70
|
-
this.CHROME_141, this.CHROME_133,
|
|
71
|
-
this.IOS_CHROME_143, this.ANDROID_CHROME_143,
|
|
68
|
+
this.CHROME_141, this.CHROME_133,
|
|
69
|
+
this.IOS_CHROME_143, this.IOS_CHROME_144, this.ANDROID_CHROME_143, this.ANDROID_CHROME_144,
|
|
72
70
|
this.FIREFOX_133,
|
|
73
|
-
this.SAFARI_18, this.IOS_SAFARI_17,
|
|
71
|
+
this.SAFARI_18, this.IOS_SAFARI_17, this.IOS_SAFARI_18,
|
|
74
72
|
];
|
|
75
73
|
},
|
|
76
74
|
};
|
|
@@ -105,16 +103,51 @@ const HTTP_STATUS_PHRASES = {
|
|
|
105
103
|
*/
|
|
106
104
|
class Cookie {
|
|
107
105
|
/**
|
|
108
|
-
* @param {
|
|
109
|
-
* @param {string}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
* @param {Object} data - Cookie data from response
|
|
107
|
+
* @param {string} data.name - Cookie name
|
|
108
|
+
* @param {string} data.value - Cookie value
|
|
109
|
+
* @param {string} [data.domain] - Cookie domain
|
|
110
|
+
* @param {string} [data.path] - Cookie path
|
|
111
|
+
* @param {string} [data.expires] - Expiration date (RFC1123 format)
|
|
112
|
+
* @param {number} [data.max_age] - Max age in seconds
|
|
113
|
+
* @param {boolean} [data.secure] - Secure flag
|
|
114
|
+
* @param {boolean} [data.http_only] - HttpOnly flag
|
|
115
|
+
* @param {string} [data.same_site] - SameSite attribute (Strict, Lax, None)
|
|
116
|
+
*/
|
|
117
|
+
constructor(data) {
|
|
118
|
+
if (typeof data === 'string') {
|
|
119
|
+
// Legacy: constructor(name, value)
|
|
120
|
+
this.name = data;
|
|
121
|
+
this.value = arguments[1] || "";
|
|
122
|
+
this.domain = "";
|
|
123
|
+
this.path = "";
|
|
124
|
+
this.expires = "";
|
|
125
|
+
this.maxAge = 0;
|
|
126
|
+
this.secure = false;
|
|
127
|
+
this.httpOnly = false;
|
|
128
|
+
this.sameSite = "";
|
|
129
|
+
} else {
|
|
130
|
+
this.name = data.name || "";
|
|
131
|
+
this.value = data.value || "";
|
|
132
|
+
this.domain = data.domain || "";
|
|
133
|
+
this.path = data.path || "";
|
|
134
|
+
this.expires = data.expires || "";
|
|
135
|
+
this.maxAge = data.max_age || 0;
|
|
136
|
+
this.secure = data.secure || false;
|
|
137
|
+
this.httpOnly = data.http_only || false;
|
|
138
|
+
this.sameSite = data.same_site || "";
|
|
139
|
+
}
|
|
114
140
|
}
|
|
115
141
|
|
|
116
142
|
toString() {
|
|
117
|
-
|
|
143
|
+
let str = `Cookie(name=${this.name}, value=${this.value}`;
|
|
144
|
+
if (this.domain) str += `, domain=${this.domain}`;
|
|
145
|
+
if (this.path) str += `, path=${this.path}`;
|
|
146
|
+
if (this.secure) str += `, secure`;
|
|
147
|
+
if (this.httpOnly) str += `, httpOnly`;
|
|
148
|
+
if (this.sameSite) str += `, sameSite=${this.sameSite}`;
|
|
149
|
+
str += `)`;
|
|
150
|
+
return str;
|
|
118
151
|
}
|
|
119
152
|
}
|
|
120
153
|
|
|
@@ -156,7 +189,7 @@ class Response {
|
|
|
156
189
|
this.elapsed = elapsed; // milliseconds
|
|
157
190
|
|
|
158
191
|
// Parse cookies from response
|
|
159
|
-
this._cookies = (data.cookies || []).map(c => new Cookie(c
|
|
192
|
+
this._cookies = (data.cookies || []).map(c => new Cookie(c));
|
|
160
193
|
|
|
161
194
|
// Parse redirect history
|
|
162
195
|
this._history = (data.history || []).map(h => new RedirectInfo(
|
|
@@ -343,7 +376,7 @@ class FastResponse {
|
|
|
343
376
|
this.elapsed = elapsed;
|
|
344
377
|
|
|
345
378
|
// Parse cookies from response
|
|
346
|
-
this._cookies = (metadata.cookies || []).map(c => new Cookie(c
|
|
379
|
+
this._cookies = (metadata.cookies || []).map(c => new Cookie(c));
|
|
347
380
|
|
|
348
381
|
// Parse redirect history
|
|
349
382
|
this._history = (metadata.history || []).map(h => new RedirectInfo(
|
|
@@ -465,7 +498,7 @@ class StreamResponse {
|
|
|
465
498
|
this.finalUrl = metadata.final_url || "";
|
|
466
499
|
this.protocol = metadata.protocol || "";
|
|
467
500
|
this.contentLength = metadata.content_length || -1;
|
|
468
|
-
this._cookies = (metadata.cookies || []).map(c => new Cookie(c
|
|
501
|
+
this._cookies = (metadata.cookies || []).map(c => new Cookie(c));
|
|
469
502
|
this._closed = false;
|
|
470
503
|
}
|
|
471
504
|
|
|
@@ -693,7 +726,7 @@ function getLibPath() {
|
|
|
693
726
|
// Define callback proto globally for koffi (must be before getLib)
|
|
694
727
|
const AsyncCallbackProto = koffi.proto("void AsyncCallback(int64 callbackId, str responseJson, str error)");
|
|
695
728
|
|
|
696
|
-
// Session cache callback prototypes
|
|
729
|
+
// Session cache callback prototypes (SYNC mode - for sync callbacks like Map)
|
|
697
730
|
const SessionCacheGetProto = koffi.proto("str SessionCacheGet(str key)");
|
|
698
731
|
const SessionCachePutProto = koffi.proto("int SessionCachePut(str key, str valueJson, int64 ttlSeconds)");
|
|
699
732
|
const SessionCacheDeleteProto = koffi.proto("int SessionCacheDelete(str key)");
|
|
@@ -701,6 +734,13 @@ const SessionCacheErrorProto = koffi.proto("void SessionCacheError(str operation
|
|
|
701
734
|
const EchCacheGetProto = koffi.proto("str EchCacheGet(str key)");
|
|
702
735
|
const EchCachePutProto = koffi.proto("int EchCachePut(str key, str valueBase64, int64 ttlSeconds)");
|
|
703
736
|
|
|
737
|
+
// Session cache callback prototypes (ASYNC mode - for async callbacks like Redis)
|
|
738
|
+
const AsyncCacheGetProto = koffi.proto("void AsyncCacheGet(int64 requestId, str key)");
|
|
739
|
+
const AsyncCachePutProto = koffi.proto("void AsyncCachePut(int64 requestId, str key, str valueJson, int64 ttlSeconds)");
|
|
740
|
+
const AsyncCacheDeleteProto = koffi.proto("void AsyncCacheDelete(int64 requestId, str key)");
|
|
741
|
+
const AsyncEchGetProto = koffi.proto("void AsyncEchGet(int64 requestId, str key)");
|
|
742
|
+
const AsyncEchPutProto = koffi.proto("void AsyncEchPut(int64 requestId, str key, str valueBase64, int64 ttlSeconds)");
|
|
743
|
+
|
|
704
744
|
// Load the native library
|
|
705
745
|
let lib = null;
|
|
706
746
|
let nativeLibHandle = null;
|
|
@@ -715,6 +755,8 @@ function getLib() {
|
|
|
715
755
|
lib = {
|
|
716
756
|
httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
|
|
717
757
|
httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
|
|
758
|
+
httpcloak_session_refresh: nativeLibHandle.func("httpcloak_session_refresh", "void", ["int64"]),
|
|
759
|
+
httpcloak_session_refresh_protocol: nativeLibHandle.func("httpcloak_session_refresh_protocol", "str", ["int64", "str"]),
|
|
718
760
|
httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
|
|
719
761
|
httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
|
|
720
762
|
httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
|
|
@@ -781,6 +823,18 @@ function getLib() {
|
|
|
781
823
|
koffi.pointer(SessionCacheErrorProto),
|
|
782
824
|
]),
|
|
783
825
|
httpcloak_clear_session_cache_callbacks: nativeLibHandle.func("httpcloak_clear_session_cache_callbacks", "void", []),
|
|
826
|
+
// Async session cache callbacks (for async backends like Redis)
|
|
827
|
+
httpcloak_set_async_session_cache_callbacks: nativeLibHandle.func("httpcloak_set_async_session_cache_callbacks", "void", [
|
|
828
|
+
koffi.pointer(AsyncCacheGetProto),
|
|
829
|
+
koffi.pointer(AsyncCachePutProto),
|
|
830
|
+
koffi.pointer(AsyncCacheDeleteProto),
|
|
831
|
+
koffi.pointer(AsyncEchGetProto),
|
|
832
|
+
koffi.pointer(AsyncEchPutProto),
|
|
833
|
+
koffi.pointer(SessionCacheErrorProto),
|
|
834
|
+
]),
|
|
835
|
+
// Async cache result functions (called by JS to provide results to Go)
|
|
836
|
+
httpcloak_async_cache_get_result: nativeLibHandle.func("httpcloak_async_cache_get_result", "void", ["int64", "str"]),
|
|
837
|
+
httpcloak_async_cache_op_result: nativeLibHandle.func("httpcloak_async_cache_op_result", "void", ["int64", "int"]),
|
|
784
838
|
};
|
|
785
839
|
}
|
|
786
840
|
return lib;
|
|
@@ -1078,7 +1132,9 @@ function version() {
|
|
|
1078
1132
|
}
|
|
1079
1133
|
|
|
1080
1134
|
/**
|
|
1081
|
-
* Get
|
|
1135
|
+
* Get available browser presets with their supported protocols.
|
|
1136
|
+
* Returns an object mapping preset names to their info:
|
|
1137
|
+
* { "chrome-144": { protocols: ["h1", "h2", "h3"] }, ... }
|
|
1082
1138
|
*/
|
|
1083
1139
|
function availablePresets() {
|
|
1084
1140
|
const nativeLib = getLib();
|
|
@@ -1087,7 +1143,7 @@ function availablePresets() {
|
|
|
1087
1143
|
if (result) {
|
|
1088
1144
|
return JSON.parse(result);
|
|
1089
1145
|
}
|
|
1090
|
-
return
|
|
1146
|
+
return {};
|
|
1091
1147
|
}
|
|
1092
1148
|
|
|
1093
1149
|
/**
|
|
@@ -1184,6 +1240,10 @@ class Session {
|
|
|
1184
1240
|
echConfigDomain = null,
|
|
1185
1241
|
tlsOnly = false,
|
|
1186
1242
|
quicIdleTimeout = 0,
|
|
1243
|
+
localAddress = null,
|
|
1244
|
+
keyLogFile = null,
|
|
1245
|
+
disableSpeculativeTls = false,
|
|
1246
|
+
switchProtocol = null,
|
|
1187
1247
|
} = options;
|
|
1188
1248
|
|
|
1189
1249
|
this._lib = getLib();
|
|
@@ -1238,6 +1298,18 @@ class Session {
|
|
|
1238
1298
|
if (quicIdleTimeout > 0) {
|
|
1239
1299
|
config.quic_idle_timeout = quicIdleTimeout;
|
|
1240
1300
|
}
|
|
1301
|
+
if (localAddress) {
|
|
1302
|
+
config.local_address = localAddress;
|
|
1303
|
+
}
|
|
1304
|
+
if (keyLogFile) {
|
|
1305
|
+
config.key_log_file = keyLogFile;
|
|
1306
|
+
}
|
|
1307
|
+
if (disableSpeculativeTls) {
|
|
1308
|
+
config.disable_speculative_tls = true;
|
|
1309
|
+
}
|
|
1310
|
+
if (switchProtocol) {
|
|
1311
|
+
config.switch_protocol = switchProtocol;
|
|
1312
|
+
}
|
|
1241
1313
|
|
|
1242
1314
|
this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
|
|
1243
1315
|
|
|
@@ -1256,6 +1328,29 @@ class Session {
|
|
|
1256
1328
|
}
|
|
1257
1329
|
}
|
|
1258
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* Refresh the session by closing all connections while keeping TLS session tickets.
|
|
1333
|
+
* This simulates a browser page refresh - connections are severed but 0-RTT
|
|
1334
|
+
* early data can be used on reconnection due to preserved session tickets.
|
|
1335
|
+
* @param {string} [switchProtocol] - Optional protocol to switch to ("h1", "h2", "h3").
|
|
1336
|
+
* Overrides any switchProtocol set at construction time. Persists for future refresh() calls.
|
|
1337
|
+
*/
|
|
1338
|
+
refresh(switchProtocol) {
|
|
1339
|
+
if (this._handle) {
|
|
1340
|
+
if (switchProtocol) {
|
|
1341
|
+
const result = this._lib.httpcloak_session_refresh_protocol(this._handle, switchProtocol);
|
|
1342
|
+
if (result) {
|
|
1343
|
+
const data = JSON.parse(result);
|
|
1344
|
+
if (data.error) {
|
|
1345
|
+
throw new HTTPCloakError(data.error);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
this._lib.httpcloak_session_refresh(this._handle);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1259
1354
|
/**
|
|
1260
1355
|
* Merge session headers with request headers
|
|
1261
1356
|
*/
|
|
@@ -2700,6 +2795,11 @@ function request(method, url, options = {}) {
|
|
|
2700
2795
|
* Use this to transparently apply fingerprinting to any HTTP client (e.g., Undici, fetch).
|
|
2701
2796
|
*
|
|
2702
2797
|
* Supports per-request proxy rotation via X-Upstream-Proxy header.
|
|
2798
|
+
* Supports per-request session routing via X-HTTPCloak-Session header.
|
|
2799
|
+
*
|
|
2800
|
+
* IMPORTANT: For distributed session caching to work with X-HTTPCloak-Session header,
|
|
2801
|
+
* you MUST register the session with the proxy using registerSession() first.
|
|
2802
|
+
* Without registration, cache callbacks will not be triggered for that session.
|
|
2703
2803
|
*
|
|
2704
2804
|
* @example
|
|
2705
2805
|
* const proxy = new LocalProxy({ preset: "chrome-143", tlsOnly: true });
|
|
@@ -2709,6 +2809,22 @@ function request(method, url, options = {}) {
|
|
|
2709
2809
|
* // Pass X-Upstream-Proxy header to rotate proxies per-request
|
|
2710
2810
|
*
|
|
2711
2811
|
* proxy.close();
|
|
2812
|
+
*
|
|
2813
|
+
* @example
|
|
2814
|
+
* // Using with distributed session cache
|
|
2815
|
+
* const proxy = new LocalProxy({ port: 8888 });
|
|
2816
|
+
* const session = new Session({ preset: 'chrome-143' });
|
|
2817
|
+
*
|
|
2818
|
+
* // Configure distributed cache (e.g., Redis)
|
|
2819
|
+
* httpcloak.configureSessionCache({
|
|
2820
|
+
* get: async (key) => await redis.get(key),
|
|
2821
|
+
* put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
|
|
2822
|
+
* });
|
|
2823
|
+
*
|
|
2824
|
+
* // REQUIRED: Register session with proxy for cache callbacks to work
|
|
2825
|
+
* proxy.registerSession('session-1', session);
|
|
2826
|
+
*
|
|
2827
|
+
* // Now requests with X-HTTPCloak-Session: session-1 will trigger cache callbacks
|
|
2712
2828
|
*/
|
|
2713
2829
|
class LocalProxy {
|
|
2714
2830
|
/**
|
|
@@ -2898,13 +3014,17 @@ class SessionCacheBackend {
|
|
|
2898
3014
|
/**
|
|
2899
3015
|
* Create a session cache backend.
|
|
2900
3016
|
*
|
|
3017
|
+
* Supports both synchronous and asynchronous callbacks. Async mode is automatically
|
|
3018
|
+
* detected when any callback is an async function or returns a Promise.
|
|
3019
|
+
*
|
|
2901
3020
|
* @param {Object} options Cache configuration
|
|
2902
|
-
* @param {Function} [options.get]
|
|
2903
|
-
* @param {Function} [options.put]
|
|
2904
|
-
* @param {Function} [options.delete]
|
|
2905
|
-
* @param {Function} [options.getEch]
|
|
2906
|
-
* @param {Function} [options.putEch]
|
|
3021
|
+
* @param {Function} [options.get] Get session data: (key: string) => string|null|Promise<string|null>
|
|
3022
|
+
* @param {Function} [options.put] Store session: (key: string, value: string, ttlSeconds: number) => number|Promise<number>
|
|
3023
|
+
* @param {Function} [options.delete] Delete session: (key: string) => number|Promise<number>
|
|
3024
|
+
* @param {Function} [options.getEch] Get ECH config: (key: string) => string|null|Promise<string|null>
|
|
3025
|
+
* @param {Function} [options.putEch] Store ECH: (key: string, value: string, ttlSeconds: number) => number|Promise<number>
|
|
2907
3026
|
* @param {Function} [options.onError] Error callback: (operation: string, key: string, error: string) => void
|
|
3027
|
+
* @param {boolean} [options.async] Force async mode (auto-detected if not specified)
|
|
2908
3028
|
*/
|
|
2909
3029
|
constructor(options = {}) {
|
|
2910
3030
|
this._get = options.get || null;
|
|
@@ -2914,6 +3034,23 @@ class SessionCacheBackend {
|
|
|
2914
3034
|
this._putEch = options.putEch || null;
|
|
2915
3035
|
this._onError = options.onError || null;
|
|
2916
3036
|
this._registered = false;
|
|
3037
|
+
|
|
3038
|
+
// Auto-detect async mode based on function types
|
|
3039
|
+
// AsyncFunction.constructor.name === 'AsyncFunction'
|
|
3040
|
+
// Use !! to ensure boolean result (null && x returns null, not false)
|
|
3041
|
+
const isAsyncFn = (fn) => !!(fn && (fn.constructor.name === 'AsyncFunction' || fn[Symbol.toStringTag] === 'AsyncFunction'));
|
|
3042
|
+
this._asyncMode = options.async !== undefined ? options.async : (
|
|
3043
|
+
isAsyncFn(this._get) || isAsyncFn(this._put) || isAsyncFn(this._delete) ||
|
|
3044
|
+
isAsyncFn(this._getEch) || isAsyncFn(this._putEch)
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
/**
|
|
3049
|
+
* Check if this backend is running in async mode.
|
|
3050
|
+
* @returns {boolean}
|
|
3051
|
+
*/
|
|
3052
|
+
get isAsync() {
|
|
3053
|
+
return this._asyncMode;
|
|
2917
3054
|
}
|
|
2918
3055
|
|
|
2919
3056
|
/**
|
|
@@ -2926,17 +3063,42 @@ class SessionCacheBackend {
|
|
|
2926
3063
|
const lib = getLib();
|
|
2927
3064
|
|
|
2928
3065
|
// Create callback pointers (keep references to prevent GC)
|
|
2929
|
-
// Use no-op callbacks for missing functions to avoid null pointer issues
|
|
2930
3066
|
_sessionCacheCallbackPtrs = {};
|
|
2931
3067
|
|
|
2932
|
-
// Create
|
|
3068
|
+
// Create error callback (shared between sync and async modes)
|
|
3069
|
+
const errorFn = this._onError;
|
|
3070
|
+
_sessionCacheCallbackPtrs.error = koffi.register((operation, key, error) => {
|
|
3071
|
+
if (!errorFn) return;
|
|
3072
|
+
try {
|
|
3073
|
+
errorFn(operation, key, error);
|
|
3074
|
+
} catch (e) {
|
|
3075
|
+
// Ignore errors in error callback
|
|
3076
|
+
}
|
|
3077
|
+
}, koffi.pointer(SessionCacheErrorProto));
|
|
3078
|
+
|
|
3079
|
+
if (this._asyncMode) {
|
|
3080
|
+
this._registerAsync(lib);
|
|
3081
|
+
} else {
|
|
3082
|
+
this._registerSync(lib);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
_sessionCacheBackend = this;
|
|
3086
|
+
this._registered = true;
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
/**
|
|
3090
|
+
* Register synchronous callbacks (for in-memory Map, etc.)
|
|
3091
|
+
* @private
|
|
3092
|
+
*/
|
|
3093
|
+
_registerSync(lib) {
|
|
2933
3094
|
const getFn = this._get;
|
|
2934
3095
|
_sessionCacheCallbackPtrs.get = koffi.register((key) => {
|
|
2935
3096
|
if (!getFn) return null;
|
|
2936
3097
|
try {
|
|
2937
3098
|
const result = getFn(key);
|
|
3099
|
+
// If sync mode but user passed async function, warn and return null
|
|
2938
3100
|
if (result && typeof result.then === 'function') {
|
|
2939
|
-
console.warn('SessionCacheBackend:
|
|
3101
|
+
console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
|
|
2940
3102
|
return null;
|
|
2941
3103
|
}
|
|
2942
3104
|
return result || null;
|
|
@@ -2945,13 +3107,13 @@ class SessionCacheBackend {
|
|
|
2945
3107
|
}
|
|
2946
3108
|
}, koffi.pointer(SessionCacheGetProto));
|
|
2947
3109
|
|
|
2948
|
-
// Create put callback (or no-op)
|
|
2949
3110
|
const putFn = this._put;
|
|
2950
3111
|
_sessionCacheCallbackPtrs.put = koffi.register((key, value, ttlSeconds) => {
|
|
2951
3112
|
if (!putFn) return 0;
|
|
2952
3113
|
try {
|
|
2953
3114
|
const result = putFn(key, value, Number(ttlSeconds));
|
|
2954
3115
|
if (result && typeof result.then === 'function') {
|
|
3116
|
+
console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
|
|
2955
3117
|
return 0;
|
|
2956
3118
|
}
|
|
2957
3119
|
return result || 0;
|
|
@@ -2960,13 +3122,13 @@ class SessionCacheBackend {
|
|
|
2960
3122
|
}
|
|
2961
3123
|
}, koffi.pointer(SessionCachePutProto));
|
|
2962
3124
|
|
|
2963
|
-
// Create delete callback (or no-op)
|
|
2964
3125
|
const deleteFn = this._delete;
|
|
2965
3126
|
_sessionCacheCallbackPtrs.delete = koffi.register((key) => {
|
|
2966
3127
|
if (!deleteFn) return 0;
|
|
2967
3128
|
try {
|
|
2968
3129
|
const result = deleteFn(key);
|
|
2969
3130
|
if (result && typeof result.then === 'function') {
|
|
3131
|
+
console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
|
|
2970
3132
|
return 0;
|
|
2971
3133
|
}
|
|
2972
3134
|
return result || 0;
|
|
@@ -2975,13 +3137,13 @@ class SessionCacheBackend {
|
|
|
2975
3137
|
}
|
|
2976
3138
|
}, koffi.pointer(SessionCacheDeleteProto));
|
|
2977
3139
|
|
|
2978
|
-
// Create ECH get callback (or no-op)
|
|
2979
3140
|
const getEchFn = this._getEch;
|
|
2980
3141
|
_sessionCacheCallbackPtrs.getEch = koffi.register((key) => {
|
|
2981
3142
|
if (!getEchFn) return null;
|
|
2982
3143
|
try {
|
|
2983
3144
|
const result = getEchFn(key);
|
|
2984
3145
|
if (result && typeof result.then === 'function') {
|
|
3146
|
+
console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
|
|
2985
3147
|
return null;
|
|
2986
3148
|
}
|
|
2987
3149
|
return result || null;
|
|
@@ -2990,13 +3152,13 @@ class SessionCacheBackend {
|
|
|
2990
3152
|
}
|
|
2991
3153
|
}, koffi.pointer(EchCacheGetProto));
|
|
2992
3154
|
|
|
2993
|
-
// Create ECH put callback (or no-op)
|
|
2994
3155
|
const putEchFn = this._putEch;
|
|
2995
3156
|
_sessionCacheCallbackPtrs.putEch = koffi.register((key, value, ttlSeconds) => {
|
|
2996
3157
|
if (!putEchFn) return 0;
|
|
2997
3158
|
try {
|
|
2998
3159
|
const result = putEchFn(key, value, Number(ttlSeconds));
|
|
2999
3160
|
if (result && typeof result.then === 'function') {
|
|
3161
|
+
console.warn('SessionCacheBackend: Detected async callback in sync mode. Use async: true option.');
|
|
3000
3162
|
return 0;
|
|
3001
3163
|
}
|
|
3002
3164
|
return result || 0;
|
|
@@ -3005,18 +3167,6 @@ class SessionCacheBackend {
|
|
|
3005
3167
|
}
|
|
3006
3168
|
}, koffi.pointer(EchCachePutProto));
|
|
3007
3169
|
|
|
3008
|
-
// Create error callback (or no-op)
|
|
3009
|
-
const errorFn = this._onError;
|
|
3010
|
-
_sessionCacheCallbackPtrs.error = koffi.register((operation, key, error) => {
|
|
3011
|
-
if (!errorFn) return;
|
|
3012
|
-
try {
|
|
3013
|
-
errorFn(operation, key, error);
|
|
3014
|
-
} catch (e) {
|
|
3015
|
-
// Ignore errors in error callback
|
|
3016
|
-
}
|
|
3017
|
-
}, koffi.pointer(SessionCacheErrorProto));
|
|
3018
|
-
|
|
3019
|
-
// Register with library
|
|
3020
3170
|
lib.httpcloak_set_session_cache_callbacks(
|
|
3021
3171
|
_sessionCacheCallbackPtrs.get,
|
|
3022
3172
|
_sessionCacheCallbackPtrs.put,
|
|
@@ -3025,9 +3175,104 @@ class SessionCacheBackend {
|
|
|
3025
3175
|
_sessionCacheCallbackPtrs.putEch,
|
|
3026
3176
|
_sessionCacheCallbackPtrs.error
|
|
3027
3177
|
);
|
|
3178
|
+
}
|
|
3028
3179
|
|
|
3029
|
-
|
|
3030
|
-
|
|
3180
|
+
/**
|
|
3181
|
+
* Register asynchronous callbacks (for Redis, database, etc.)
|
|
3182
|
+
* Go will call our callback with a request ID, we process async,
|
|
3183
|
+
* then call back into Go with the result.
|
|
3184
|
+
* @private
|
|
3185
|
+
*/
|
|
3186
|
+
_registerAsync(lib) {
|
|
3187
|
+
const getFn = this._get;
|
|
3188
|
+
_sessionCacheCallbackPtrs.get = koffi.register((requestId, key) => {
|
|
3189
|
+
if (!getFn) {
|
|
3190
|
+
lib.httpcloak_async_cache_get_result(requestId, null);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
// Process async and call back with result
|
|
3194
|
+
Promise.resolve()
|
|
3195
|
+
.then(() => getFn(key))
|
|
3196
|
+
.then((result) => {
|
|
3197
|
+
lib.httpcloak_async_cache_get_result(requestId, result || null);
|
|
3198
|
+
})
|
|
3199
|
+
.catch(() => {
|
|
3200
|
+
lib.httpcloak_async_cache_get_result(requestId, null);
|
|
3201
|
+
});
|
|
3202
|
+
}, koffi.pointer(AsyncCacheGetProto));
|
|
3203
|
+
|
|
3204
|
+
const putFn = this._put;
|
|
3205
|
+
_sessionCacheCallbackPtrs.put = koffi.register((requestId, key, value, ttlSeconds) => {
|
|
3206
|
+
if (!putFn) {
|
|
3207
|
+
lib.httpcloak_async_cache_op_result(requestId, 0);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
Promise.resolve()
|
|
3211
|
+
.then(() => putFn(key, value, Number(ttlSeconds)))
|
|
3212
|
+
.then((result) => {
|
|
3213
|
+
lib.httpcloak_async_cache_op_result(requestId, result || 0);
|
|
3214
|
+
})
|
|
3215
|
+
.catch(() => {
|
|
3216
|
+
lib.httpcloak_async_cache_op_result(requestId, -1);
|
|
3217
|
+
});
|
|
3218
|
+
}, koffi.pointer(AsyncCachePutProto));
|
|
3219
|
+
|
|
3220
|
+
const deleteFn = this._delete;
|
|
3221
|
+
_sessionCacheCallbackPtrs.delete = koffi.register((requestId, key) => {
|
|
3222
|
+
if (!deleteFn) {
|
|
3223
|
+
lib.httpcloak_async_cache_op_result(requestId, 0);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
Promise.resolve()
|
|
3227
|
+
.then(() => deleteFn(key))
|
|
3228
|
+
.then((result) => {
|
|
3229
|
+
lib.httpcloak_async_cache_op_result(requestId, result || 0);
|
|
3230
|
+
})
|
|
3231
|
+
.catch(() => {
|
|
3232
|
+
lib.httpcloak_async_cache_op_result(requestId, -1);
|
|
3233
|
+
});
|
|
3234
|
+
}, koffi.pointer(AsyncCacheDeleteProto));
|
|
3235
|
+
|
|
3236
|
+
const getEchFn = this._getEch;
|
|
3237
|
+
_sessionCacheCallbackPtrs.getEch = koffi.register((requestId, key) => {
|
|
3238
|
+
if (!getEchFn) {
|
|
3239
|
+
lib.httpcloak_async_cache_get_result(requestId, null);
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
Promise.resolve()
|
|
3243
|
+
.then(() => getEchFn(key))
|
|
3244
|
+
.then((result) => {
|
|
3245
|
+
lib.httpcloak_async_cache_get_result(requestId, result || null);
|
|
3246
|
+
})
|
|
3247
|
+
.catch(() => {
|
|
3248
|
+
lib.httpcloak_async_cache_get_result(requestId, null);
|
|
3249
|
+
});
|
|
3250
|
+
}, koffi.pointer(AsyncEchGetProto));
|
|
3251
|
+
|
|
3252
|
+
const putEchFn = this._putEch;
|
|
3253
|
+
_sessionCacheCallbackPtrs.putEch = koffi.register((requestId, key, value, ttlSeconds) => {
|
|
3254
|
+
if (!putEchFn) {
|
|
3255
|
+
lib.httpcloak_async_cache_op_result(requestId, 0);
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
Promise.resolve()
|
|
3259
|
+
.then(() => putEchFn(key, value, Number(ttlSeconds)))
|
|
3260
|
+
.then((result) => {
|
|
3261
|
+
lib.httpcloak_async_cache_op_result(requestId, result || 0);
|
|
3262
|
+
})
|
|
3263
|
+
.catch(() => {
|
|
3264
|
+
lib.httpcloak_async_cache_op_result(requestId, -1);
|
|
3265
|
+
});
|
|
3266
|
+
}, koffi.pointer(AsyncEchPutProto));
|
|
3267
|
+
|
|
3268
|
+
lib.httpcloak_set_async_session_cache_callbacks(
|
|
3269
|
+
_sessionCacheCallbackPtrs.get,
|
|
3270
|
+
_sessionCacheCallbackPtrs.put,
|
|
3271
|
+
_sessionCacheCallbackPtrs.delete,
|
|
3272
|
+
_sessionCacheCallbackPtrs.getEch,
|
|
3273
|
+
_sessionCacheCallbackPtrs.putEch,
|
|
3274
|
+
_sessionCacheCallbackPtrs.error
|
|
3275
|
+
);
|
|
3031
3276
|
}
|
|
3032
3277
|
|
|
3033
3278
|
/**
|
|
@@ -3053,11 +3298,25 @@ class SessionCacheBackend {
|
|
|
3053
3298
|
* Configure a distributed session cache backend.
|
|
3054
3299
|
*
|
|
3055
3300
|
* This is a convenience function that creates and registers a SessionCacheBackend.
|
|
3301
|
+
* Supports both synchronous and asynchronous callbacks (auto-detected).
|
|
3056
3302
|
*
|
|
3057
3303
|
* @param {Object} options Cache configuration (same as SessionCacheBackend constructor)
|
|
3058
3304
|
* @returns {SessionCacheBackend} The registered backend
|
|
3059
3305
|
*
|
|
3060
|
-
* Example:
|
|
3306
|
+
* Example using in-memory Map (sync):
|
|
3307
|
+
* ```javascript
|
|
3308
|
+
* const httpcloak = require('httpcloak');
|
|
3309
|
+
*
|
|
3310
|
+
* const cache = new Map();
|
|
3311
|
+
*
|
|
3312
|
+
* httpcloak.configureSessionCache({
|
|
3313
|
+
* get: (key) => cache.get(key) || null,
|
|
3314
|
+
* put: (key, value, ttl) => { cache.set(key, value); return 0; },
|
|
3315
|
+
* delete: (key) => { cache.delete(key); return 0; },
|
|
3316
|
+
* });
|
|
3317
|
+
* ```
|
|
3318
|
+
*
|
|
3319
|
+
* Example using Redis (async):
|
|
3061
3320
|
* ```javascript
|
|
3062
3321
|
* const Redis = require('ioredis');
|
|
3063
3322
|
* const httpcloak = require('httpcloak');
|
|
@@ -3065,12 +3324,12 @@ class SessionCacheBackend {
|
|
|
3065
3324
|
* const redis = new Redis();
|
|
3066
3325
|
*
|
|
3067
3326
|
* httpcloak.configureSessionCache({
|
|
3068
|
-
* get: (key) => redis.get(key),
|
|
3069
|
-
* put: (key, value, ttl) => { redis.setex(key, ttl, value); return 0; },
|
|
3070
|
-
* delete: (key) => { redis.del(key); return 0; },
|
|
3327
|
+
* get: async (key) => await redis.get(key),
|
|
3328
|
+
* put: async (key, value, ttl) => { await redis.setex(key, ttl, value); return 0; },
|
|
3329
|
+
* delete: async (key) => { await redis.del(key); return 0; },
|
|
3071
3330
|
* });
|
|
3072
3331
|
*
|
|
3073
|
-
* //
|
|
3332
|
+
* // Async callbacks are automatically detected and handled properly
|
|
3074
3333
|
* const session = new httpcloak.Session();
|
|
3075
3334
|
* await session.get('https://example.com');
|
|
3076
3335
|
* ```
|
package/lib/index.mjs
CHANGED
|
@@ -12,11 +12,26 @@ const require = createRequire(import.meta.url);
|
|
|
12
12
|
const cjs = require("./index.js");
|
|
13
13
|
|
|
14
14
|
// Re-export all named exports
|
|
15
|
+
// Classes
|
|
15
16
|
export const Session = cjs.Session;
|
|
17
|
+
export const LocalProxy = cjs.LocalProxy;
|
|
16
18
|
export const Response = cjs.Response;
|
|
19
|
+
export const FastResponse = cjs.FastResponse;
|
|
20
|
+
export const StreamResponse = cjs.StreamResponse;
|
|
21
|
+
export const Cookie = cjs.Cookie;
|
|
22
|
+
export const RedirectInfo = cjs.RedirectInfo;
|
|
17
23
|
export const HTTPCloakError = cjs.HTTPCloakError;
|
|
24
|
+
export const SessionCacheBackend = cjs.SessionCacheBackend;
|
|
25
|
+
|
|
26
|
+
// Presets
|
|
18
27
|
export const Preset = cjs.Preset;
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
19
30
|
export const configure = cjs.configure;
|
|
31
|
+
export const configureSessionCache = cjs.configureSessionCache;
|
|
32
|
+
export const clearSessionCache = cjs.clearSessionCache;
|
|
33
|
+
|
|
34
|
+
// Module-level functions
|
|
20
35
|
export const get = cjs.get;
|
|
21
36
|
export const post = cjs.post;
|
|
22
37
|
export const put = cjs.put;
|
|
@@ -24,9 +39,15 @@ export const patch = cjs.patch;
|
|
|
24
39
|
export const head = cjs.head;
|
|
25
40
|
export const options = cjs.options;
|
|
26
41
|
export const request = cjs.request;
|
|
42
|
+
|
|
43
|
+
// Utility
|
|
27
44
|
export const version = cjs.version;
|
|
28
45
|
export const availablePresets = cjs.availablePresets;
|
|
29
46
|
|
|
47
|
+
// DNS configuration
|
|
48
|
+
export const setEchDnsServers = cjs.setEchDnsServers;
|
|
49
|
+
export const getEchDnsServers = cjs.getEchDnsServers;
|
|
50
|
+
|
|
30
51
|
// 'delete' is a reserved word in ESM, so we export it specially
|
|
31
52
|
const del = cjs.delete;
|
|
32
53
|
export { del as delete };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "httpcloak",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0-beta.4",
|
|
4
4
|
"description": "Browser fingerprint emulation HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.mjs",
|
|
@@ -49,11 +49,15 @@
|
|
|
49
49
|
"koffi": "^2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@httpcloak/
|
|
53
|
-
"@httpcloak/
|
|
54
|
-
"@httpcloak/
|
|
55
|
-
"@httpcloak/
|
|
56
|
-
"@httpcloak/win32-
|
|
57
|
-
"@httpcloak/win32-
|
|
52
|
+
"@httpcloak/darwin-arm64": "1.6.0-beta.4",
|
|
53
|
+
"@httpcloak/darwin-x64": "1.6.0-beta.4",
|
|
54
|
+
"@httpcloak/linux-arm64": "1.6.0-beta.4",
|
|
55
|
+
"@httpcloak/linux-x64": "1.6.0-beta.4",
|
|
56
|
+
"@httpcloak/win32-arm64": "1.6.0-beta.4",
|
|
57
|
+
"@httpcloak/win32-x64": "1.6.0-beta.4"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^25.1.0",
|
|
61
|
+
"typescript": "^5.9.3"
|
|
58
62
|
}
|
|
59
63
|
}
|