httpcloak 1.5.0 → 1.5.2
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 +111 -1
- package/lib/index.d.ts +85 -34
- package/lib/index.js +482 -56
- package/lib/index.mjs +35 -0
- package/npm/darwin-arm64/lib.mjs +6 -0
- package/npm/darwin-arm64/package.json +8 -1
- package/npm/darwin-x64/lib.mjs +6 -0
- package/npm/darwin-x64/package.json +8 -1
- package/npm/linux-arm64/lib.mjs +6 -0
- package/npm/linux-arm64/package.json +8 -1
- package/npm/linux-x64/lib.mjs +6 -0
- package/npm/linux-x64/package.json +8 -1
- package/npm/win32-arm64/lib.mjs +6 -0
- package/npm/win32-arm64/package.json +8 -1
- package/npm/win32-x64/lib.mjs +6 -0
- package/npm/win32-x64/package.json +8 -1
- package/package.json +21 -7
package/README.md
CHANGED
|
@@ -99,13 +99,123 @@ session.postCb(
|
|
|
99
99
|
);
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
## Proxy Support
|
|
103
|
+
|
|
104
|
+
HTTPCloak supports HTTP, SOCKS5, and HTTP/3 (MASQUE) proxies with full fingerprint preservation.
|
|
105
|
+
|
|
106
|
+
### HTTP Proxy
|
|
103
107
|
|
|
104
108
|
```javascript
|
|
109
|
+
const { Session } = require("httpcloak");
|
|
110
|
+
|
|
111
|
+
// Basic HTTP proxy
|
|
105
112
|
const session = new Session({
|
|
113
|
+
preset: "chrome-143",
|
|
114
|
+
proxy: "http://host:port",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// With authentication
|
|
118
|
+
const sessionAuth = new Session({
|
|
106
119
|
preset: "chrome-143",
|
|
107
120
|
proxy: "http://user:pass@host:port",
|
|
108
121
|
});
|
|
122
|
+
|
|
123
|
+
// HTTPS proxy
|
|
124
|
+
const sessionHttps = new Session({
|
|
125
|
+
preset: "chrome-143",
|
|
126
|
+
proxy: "https://user:pass@host:port",
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### SOCKS5 Proxy
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const { Session } = require("httpcloak");
|
|
134
|
+
|
|
135
|
+
// SOCKS5 proxy (with DNS resolution on proxy)
|
|
136
|
+
const session = new Session({
|
|
137
|
+
preset: "chrome-143",
|
|
138
|
+
proxy: "socks5h://host:port",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// With authentication
|
|
142
|
+
const sessionAuth = new Session({
|
|
143
|
+
preset: "chrome-143",
|
|
144
|
+
proxy: "socks5h://user:pass@host:port",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
148
|
+
console.log(response.protocol); // h3 (HTTP/3 through SOCKS5!)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### HTTP/3 MASQUE Proxy
|
|
152
|
+
|
|
153
|
+
MASQUE (RFC 9484) enables HTTP/3 connections through compatible proxies:
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
const { Session } = require("httpcloak");
|
|
157
|
+
|
|
158
|
+
// MASQUE proxy (auto-detected for known providers like Bright Data)
|
|
159
|
+
const session = new Session({
|
|
160
|
+
preset: "chrome-143",
|
|
161
|
+
proxy: "https://user:pass@brd.superproxy.io:10001",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
165
|
+
console.log(response.protocol); // h3
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Advanced Features
|
|
169
|
+
|
|
170
|
+
### Encrypted Client Hello (ECH)
|
|
171
|
+
|
|
172
|
+
ECH encrypts the SNI (Server Name Indication) to prevent traffic analysis. Works with all Cloudflare domains:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
const { Session } = require("httpcloak");
|
|
176
|
+
|
|
177
|
+
// Enable ECH for Cloudflare domains
|
|
178
|
+
const session = new Session({
|
|
179
|
+
preset: "chrome-143",
|
|
180
|
+
echConfigDomain: "cloudflare-ech.com",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
184
|
+
console.log(response.text);
|
|
185
|
+
// Output includes: sni=encrypted, http=http/3
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Domain Fronting (Connect-To)
|
|
189
|
+
|
|
190
|
+
Connect to one server while requesting a different domain:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const { Session } = require("httpcloak");
|
|
194
|
+
|
|
195
|
+
// Connect to claude.ai's IP but request www.cloudflare.com
|
|
196
|
+
const session = new Session({
|
|
197
|
+
preset: "chrome-143",
|
|
198
|
+
connectTo: { "www.cloudflare.com": "claude.ai" },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Combined: SOCKS5 + ECH
|
|
205
|
+
|
|
206
|
+
Get HTTP/3 with encrypted SNI through a SOCKS5 proxy:
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const { Session } = require("httpcloak");
|
|
210
|
+
|
|
211
|
+
const session = new Session({
|
|
212
|
+
preset: "chrome-143",
|
|
213
|
+
proxy: "socks5h://user:pass@host:port",
|
|
214
|
+
echConfigDomain: "cloudflare-ech.com",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const response = await session.get("https://www.cloudflare.com/cdn-cgi/trace");
|
|
218
|
+
// Response shows: http=http/3, sni=encrypted
|
|
109
219
|
```
|
|
110
220
|
|
|
111
221
|
## Cookie Management
|
package/lib/index.d.ts
CHANGED
|
@@ -6,6 +6,22 @@ export class HTTPCloakError extends Error {
|
|
|
6
6
|
name: "HTTPCloakError";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export class Cookie {
|
|
10
|
+
/** Cookie name */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Cookie value */
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class RedirectInfo {
|
|
17
|
+
/** HTTP status code */
|
|
18
|
+
statusCode: number;
|
|
19
|
+
/** Request URL */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Response headers */
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
export class Response {
|
|
10
26
|
/** HTTP status code */
|
|
11
27
|
statusCode: number;
|
|
@@ -13,21 +29,40 @@ export class Response {
|
|
|
13
29
|
headers: Record<string, string>;
|
|
14
30
|
/** Raw response body as Buffer */
|
|
15
31
|
body: Buffer;
|
|
32
|
+
/** Response body as Buffer (alias for body) */
|
|
33
|
+
content: Buffer;
|
|
16
34
|
/** Response body as string */
|
|
17
35
|
text: string;
|
|
18
36
|
/** Final URL after redirects */
|
|
19
37
|
finalUrl: string;
|
|
38
|
+
/** Final URL after redirects (alias for finalUrl) */
|
|
39
|
+
url: string;
|
|
20
40
|
/** Protocol used (http/1.1, h2, h3) */
|
|
21
41
|
protocol: string;
|
|
42
|
+
/** Elapsed time in milliseconds */
|
|
43
|
+
elapsed: number;
|
|
44
|
+
/** Cookies set by this response */
|
|
45
|
+
cookies: Cookie[];
|
|
46
|
+
/** Redirect history */
|
|
47
|
+
history: RedirectInfo[];
|
|
48
|
+
/** True if status code < 400 */
|
|
49
|
+
ok: boolean;
|
|
50
|
+
/** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
|
|
51
|
+
reason: string;
|
|
52
|
+
/** Response encoding from Content-Type header */
|
|
53
|
+
encoding: string | null;
|
|
22
54
|
|
|
23
55
|
/** Parse response body as JSON */
|
|
24
56
|
json<T = any>(): T;
|
|
57
|
+
|
|
58
|
+
/** Raise error if status >= 400 */
|
|
59
|
+
raiseForStatus(): void;
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
export interface SessionOptions {
|
|
28
63
|
/** Browser preset to use (default: "chrome-143") */
|
|
29
64
|
preset?: string;
|
|
30
|
-
/** Proxy URL (e.g., "http://user:pass@host:port") */
|
|
65
|
+
/** Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port") */
|
|
31
66
|
proxy?: string;
|
|
32
67
|
/** Request timeout in seconds (default: 30) */
|
|
33
68
|
timeout?: number;
|
|
@@ -43,17 +78,33 @@ export interface SessionOptions {
|
|
|
43
78
|
retry?: number;
|
|
44
79
|
/** Status codes to retry on (default: [429, 500, 502, 503, 504]) */
|
|
45
80
|
retryOnStatus?: number[];
|
|
81
|
+
/** Prefer IPv4 addresses over IPv6 (default: false) */
|
|
82
|
+
preferIpv4?: boolean;
|
|
83
|
+
/** Default basic auth [username, password] */
|
|
84
|
+
auth?: [string, string];
|
|
85
|
+
/** Domain fronting map {requestHost: connectHost} - DNS resolves connectHost but SNI/Host uses requestHost */
|
|
86
|
+
connectTo?: Record<string, string>;
|
|
87
|
+
/** Domain to fetch ECH config from (e.g., "cloudflare-ech.com" for any Cloudflare domain) */
|
|
88
|
+
echConfigDomain?: string;
|
|
46
89
|
}
|
|
47
90
|
|
|
48
91
|
export interface RequestOptions {
|
|
49
|
-
/** HTTP method */
|
|
50
|
-
method: string;
|
|
51
|
-
/** Request URL */
|
|
52
|
-
url: string;
|
|
53
92
|
/** Optional custom headers */
|
|
54
93
|
headers?: Record<string, string>;
|
|
55
|
-
/** Optional request body */
|
|
94
|
+
/** Optional request body (for POST, PUT, PATCH) */
|
|
56
95
|
body?: string | Buffer | Record<string, any>;
|
|
96
|
+
/** JSON body (will be serialized) */
|
|
97
|
+
json?: Record<string, any>;
|
|
98
|
+
/** Form data (will be URL encoded) */
|
|
99
|
+
data?: Record<string, any>;
|
|
100
|
+
/** Files to upload as multipart/form-data */
|
|
101
|
+
files?: Record<string, Buffer | { filename: string; content: Buffer; contentType?: string }>;
|
|
102
|
+
/** Query parameters */
|
|
103
|
+
params?: Record<string, string | number | boolean>;
|
|
104
|
+
/** Cookies to send with this request */
|
|
105
|
+
cookies?: Record<string, string>;
|
|
106
|
+
/** Basic auth [username, password] */
|
|
107
|
+
auth?: [string, string];
|
|
57
108
|
/** Optional request timeout in seconds */
|
|
58
109
|
timeout?: number;
|
|
59
110
|
}
|
|
@@ -61,67 +112,66 @@ export interface RequestOptions {
|
|
|
61
112
|
export class Session {
|
|
62
113
|
constructor(options?: SessionOptions);
|
|
63
114
|
|
|
115
|
+
/** Default headers for all requests */
|
|
116
|
+
headers: Record<string, string>;
|
|
117
|
+
|
|
118
|
+
/** Default auth for all requests [username, password] */
|
|
119
|
+
auth: [string, string] | null;
|
|
120
|
+
|
|
64
121
|
/** Close the session and release resources */
|
|
65
122
|
close(): void;
|
|
66
123
|
|
|
67
124
|
// Synchronous methods
|
|
68
125
|
/** Perform a synchronous GET request */
|
|
69
|
-
getSync(url: string,
|
|
126
|
+
getSync(url: string, options?: RequestOptions): Response;
|
|
70
127
|
|
|
71
128
|
/** Perform a synchronous POST request */
|
|
72
|
-
postSync(
|
|
73
|
-
url: string,
|
|
74
|
-
body?: string | Buffer | Record<string, any>,
|
|
75
|
-
headers?: Record<string, string>
|
|
76
|
-
): Response;
|
|
129
|
+
postSync(url: string, options?: RequestOptions): Response;
|
|
77
130
|
|
|
78
131
|
/** Perform a synchronous custom HTTP request */
|
|
79
|
-
requestSync(
|
|
132
|
+
requestSync(method: string, url: string, options?: RequestOptions): Response;
|
|
80
133
|
|
|
81
134
|
// Promise-based methods
|
|
82
135
|
/** Perform an async GET request */
|
|
83
|
-
get(url: string,
|
|
136
|
+
get(url: string, options?: RequestOptions): Promise<Response>;
|
|
84
137
|
|
|
85
138
|
/** Perform an async POST request */
|
|
86
|
-
post(
|
|
87
|
-
url: string,
|
|
88
|
-
body?: string | Buffer | Record<string, any>,
|
|
89
|
-
headers?: Record<string, string>
|
|
90
|
-
): Promise<Response>;
|
|
139
|
+
post(url: string, options?: RequestOptions): Promise<Response>;
|
|
91
140
|
|
|
92
141
|
/** Perform an async custom HTTP request */
|
|
93
|
-
request(
|
|
142
|
+
request(method: string, url: string, options?: RequestOptions): Promise<Response>;
|
|
94
143
|
|
|
95
144
|
/** Perform an async PUT request */
|
|
96
|
-
put(
|
|
97
|
-
url: string,
|
|
98
|
-
body?: string | Buffer | Record<string, any>,
|
|
99
|
-
headers?: Record<string, string>
|
|
100
|
-
): Promise<Response>;
|
|
145
|
+
put(url: string, options?: RequestOptions): Promise<Response>;
|
|
101
146
|
|
|
102
147
|
/** Perform an async DELETE request */
|
|
103
|
-
delete(url: string,
|
|
148
|
+
delete(url: string, options?: RequestOptions): Promise<Response>;
|
|
104
149
|
|
|
105
150
|
/** Perform an async PATCH request */
|
|
106
|
-
patch(
|
|
107
|
-
url: string,
|
|
108
|
-
body?: string | Buffer | Record<string, any>,
|
|
109
|
-
headers?: Record<string, string>
|
|
110
|
-
): Promise<Response>;
|
|
151
|
+
patch(url: string, options?: RequestOptions): Promise<Response>;
|
|
111
152
|
|
|
112
153
|
/** Perform an async HEAD request */
|
|
113
|
-
head(url: string,
|
|
154
|
+
head(url: string, options?: RequestOptions): Promise<Response>;
|
|
114
155
|
|
|
115
156
|
/** Perform an async OPTIONS request */
|
|
116
|
-
options(url: string,
|
|
157
|
+
options(url: string, options?: RequestOptions): Promise<Response>;
|
|
117
158
|
|
|
118
159
|
// Cookie management
|
|
119
160
|
/** Get all cookies from the session */
|
|
120
161
|
getCookies(): Record<string, string>;
|
|
121
162
|
|
|
163
|
+
/** Get a specific cookie by name */
|
|
164
|
+
getCookie(name: string): string | null;
|
|
165
|
+
|
|
122
166
|
/** Set a cookie in the session */
|
|
123
167
|
setCookie(name: string, value: string): void;
|
|
124
168
|
|
|
169
|
+
/** Delete a specific cookie by name */
|
|
170
|
+
deleteCookie(name: string): void;
|
|
171
|
+
|
|
172
|
+
/** Clear all cookies from the session */
|
|
173
|
+
clearCookies(): void;
|
|
174
|
+
|
|
125
175
|
/** Get cookies as a property */
|
|
126
176
|
readonly cookies: Record<string, string>;
|
|
127
177
|
}
|
|
@@ -162,7 +212,8 @@ export function patch(url: string, options?: RequestOptions): Promise<Response>;
|
|
|
162
212
|
export function head(url: string, options?: RequestOptions): Promise<Response>;
|
|
163
213
|
|
|
164
214
|
/** Perform an OPTIONS request */
|
|
165
|
-
|
|
215
|
+
declare function opts(url: string, options?: RequestOptions): Promise<Response>;
|
|
216
|
+
export { opts as options };
|
|
166
217
|
|
|
167
218
|
/** Perform a custom HTTP request */
|
|
168
219
|
export function request(method: string, url: string, options?: RequestOptions): Promise<Response>;
|
package/lib/index.js
CHANGED
|
@@ -69,17 +69,105 @@ const Preset = {
|
|
|
69
69
|
},
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* HTTP status reason phrases
|
|
74
|
+
*/
|
|
75
|
+
const HTTP_STATUS_PHRASES = {
|
|
76
|
+
100: "Continue", 101: "Switching Protocols", 102: "Processing",
|
|
77
|
+
200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information",
|
|
78
|
+
204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status",
|
|
79
|
+
300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other",
|
|
80
|
+
304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect",
|
|
81
|
+
400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden",
|
|
82
|
+
404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable",
|
|
83
|
+
407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict",
|
|
84
|
+
410: "Gone", 411: "Length Required", 412: "Precondition Failed",
|
|
85
|
+
413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type",
|
|
86
|
+
416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a teapot",
|
|
87
|
+
421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked",
|
|
88
|
+
424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required",
|
|
89
|
+
428: "Precondition Required", 429: "Too Many Requests",
|
|
90
|
+
431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons",
|
|
91
|
+
500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway",
|
|
92
|
+
503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported",
|
|
93
|
+
506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected",
|
|
94
|
+
510: "Not Extended", 511: "Network Authentication Required",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Cookie object from Set-Cookie header
|
|
99
|
+
*/
|
|
100
|
+
class Cookie {
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} name - Cookie name
|
|
103
|
+
* @param {string} value - Cookie value
|
|
104
|
+
*/
|
|
105
|
+
constructor(name, value) {
|
|
106
|
+
this.name = name;
|
|
107
|
+
this.value = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
toString() {
|
|
111
|
+
return `Cookie(name=${this.name}, value=${this.value})`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Redirect info from history
|
|
117
|
+
*/
|
|
118
|
+
class RedirectInfo {
|
|
119
|
+
/**
|
|
120
|
+
* @param {number} statusCode - HTTP status code
|
|
121
|
+
* @param {string} url - Request URL
|
|
122
|
+
* @param {Object} headers - Response headers
|
|
123
|
+
*/
|
|
124
|
+
constructor(statusCode, url, headers) {
|
|
125
|
+
this.statusCode = statusCode;
|
|
126
|
+
this.url = url;
|
|
127
|
+
this.headers = headers || {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
toString() {
|
|
131
|
+
return `RedirectInfo(statusCode=${this.statusCode}, url=${this.url})`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
72
135
|
/**
|
|
73
136
|
* Response object returned from HTTP requests
|
|
74
137
|
*/
|
|
75
138
|
class Response {
|
|
76
|
-
|
|
139
|
+
/**
|
|
140
|
+
* @param {Object} data - Response data from native library
|
|
141
|
+
* @param {number} [elapsed=0] - Elapsed time in milliseconds
|
|
142
|
+
*/
|
|
143
|
+
constructor(data, elapsed = 0) {
|
|
77
144
|
this.statusCode = data.status_code || 0;
|
|
78
145
|
this.headers = data.headers || {};
|
|
79
146
|
this._body = Buffer.from(data.body || "", "utf8");
|
|
80
147
|
this._text = data.body || "";
|
|
81
148
|
this.finalUrl = data.final_url || "";
|
|
82
149
|
this.protocol = data.protocol || "";
|
|
150
|
+
this.elapsed = elapsed; // milliseconds
|
|
151
|
+
|
|
152
|
+
// Parse cookies from response
|
|
153
|
+
this._cookies = (data.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
|
|
154
|
+
|
|
155
|
+
// Parse redirect history
|
|
156
|
+
this._history = (data.history || []).map(h => new RedirectInfo(
|
|
157
|
+
h.status_code || 0,
|
|
158
|
+
h.url || "",
|
|
159
|
+
h.headers || {}
|
|
160
|
+
));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Cookies set by this response */
|
|
164
|
+
get cookies() {
|
|
165
|
+
return this._cookies;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Redirect history (list of RedirectInfo objects) */
|
|
169
|
+
get history() {
|
|
170
|
+
return this._history;
|
|
83
171
|
}
|
|
84
172
|
|
|
85
173
|
/** Response body as string */
|
|
@@ -107,6 +195,29 @@ class Response {
|
|
|
107
195
|
return this.statusCode < 400;
|
|
108
196
|
}
|
|
109
197
|
|
|
198
|
+
/** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
|
|
199
|
+
get reason() {
|
|
200
|
+
return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Response encoding from Content-Type header.
|
|
205
|
+
* Returns null if not specified.
|
|
206
|
+
*/
|
|
207
|
+
get encoding() {
|
|
208
|
+
let contentType = this.headers["content-type"] || this.headers["Content-Type"] || "";
|
|
209
|
+
if (contentType.includes("charset=")) {
|
|
210
|
+
const parts = contentType.split(";");
|
|
211
|
+
for (const part of parts) {
|
|
212
|
+
const trimmed = part.trim();
|
|
213
|
+
if (trimmed.toLowerCase().startsWith("charset=")) {
|
|
214
|
+
return trimmed.split("=")[1].trim().replace(/['"]/g, "");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
110
221
|
/**
|
|
111
222
|
* Parse response body as JSON
|
|
112
223
|
*/
|
|
@@ -119,7 +230,7 @@ class Response {
|
|
|
119
230
|
*/
|
|
120
231
|
raiseForStatus() {
|
|
121
232
|
if (!this.ok) {
|
|
122
|
-
throw new HTTPCloakError(`HTTP ${this.statusCode}`);
|
|
233
|
+
throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
|
|
123
234
|
}
|
|
124
235
|
}
|
|
125
236
|
}
|
|
@@ -215,32 +326,158 @@ function getLibPath() {
|
|
|
215
326
|
);
|
|
216
327
|
}
|
|
217
328
|
|
|
329
|
+
// Define callback proto globally for koffi (must be before getLib)
|
|
330
|
+
const AsyncCallbackProto = koffi.proto("void AsyncCallback(int64 callbackId, str responseJson, str error)");
|
|
331
|
+
|
|
218
332
|
// Load the native library
|
|
219
333
|
let lib = null;
|
|
334
|
+
let nativeLibHandle = null;
|
|
220
335
|
|
|
221
336
|
function getLib() {
|
|
222
337
|
if (lib === null) {
|
|
223
338
|
const libPath = getLibPath();
|
|
224
|
-
|
|
339
|
+
nativeLibHandle = koffi.load(libPath);
|
|
225
340
|
|
|
226
341
|
// Use str for string returns - koffi handles the string copy automatically
|
|
227
342
|
// Note: The C strings allocated by Go are not freed, but Go's GC handles them
|
|
228
343
|
lib = {
|
|
229
|
-
httpcloak_session_new:
|
|
230
|
-
httpcloak_session_free:
|
|
231
|
-
httpcloak_get:
|
|
232
|
-
httpcloak_post:
|
|
233
|
-
httpcloak_request:
|
|
234
|
-
httpcloak_get_cookies:
|
|
235
|
-
httpcloak_set_cookie:
|
|
236
|
-
httpcloak_free_string:
|
|
237
|
-
httpcloak_version:
|
|
238
|
-
httpcloak_available_presets:
|
|
344
|
+
httpcloak_session_new: nativeLibHandle.func("httpcloak_session_new", "int64", ["str"]),
|
|
345
|
+
httpcloak_session_free: nativeLibHandle.func("httpcloak_session_free", "void", ["int64"]),
|
|
346
|
+
httpcloak_get: nativeLibHandle.func("httpcloak_get", "str", ["int64", "str", "str"]),
|
|
347
|
+
httpcloak_post: nativeLibHandle.func("httpcloak_post", "str", ["int64", "str", "str", "str"]),
|
|
348
|
+
httpcloak_request: nativeLibHandle.func("httpcloak_request", "str", ["int64", "str"]),
|
|
349
|
+
httpcloak_get_cookies: nativeLibHandle.func("httpcloak_get_cookies", "str", ["int64"]),
|
|
350
|
+
httpcloak_set_cookie: nativeLibHandle.func("httpcloak_set_cookie", "void", ["int64", "str", "str"]),
|
|
351
|
+
httpcloak_free_string: nativeLibHandle.func("httpcloak_free_string", "void", ["void*"]),
|
|
352
|
+
httpcloak_version: nativeLibHandle.func("httpcloak_version", "str", []),
|
|
353
|
+
httpcloak_available_presets: nativeLibHandle.func("httpcloak_available_presets", "str", []),
|
|
354
|
+
// Async functions
|
|
355
|
+
httpcloak_register_callback: nativeLibHandle.func("httpcloak_register_callback", "int64", [koffi.pointer(AsyncCallbackProto)]),
|
|
356
|
+
httpcloak_unregister_callback: nativeLibHandle.func("httpcloak_unregister_callback", "void", ["int64"]),
|
|
357
|
+
httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
|
|
358
|
+
httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
|
|
359
|
+
httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
|
|
239
360
|
};
|
|
240
361
|
}
|
|
241
362
|
return lib;
|
|
242
363
|
}
|
|
243
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Async callback manager for native Go goroutine-based async
|
|
367
|
+
*
|
|
368
|
+
* Each async request registers a callback with Go and receives a unique ID.
|
|
369
|
+
* When Go completes the request, it invokes the callback with that ID.
|
|
370
|
+
*/
|
|
371
|
+
class AsyncCallbackManager {
|
|
372
|
+
constructor() {
|
|
373
|
+
// callbackId -> { resolve, reject, startTime }
|
|
374
|
+
this._pendingRequests = new Map();
|
|
375
|
+
this._callbackPtr = null;
|
|
376
|
+
this._refTimer = null; // Timer to keep event loop alive
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Ref the event loop to prevent Node.js from exiting while requests are pending
|
|
381
|
+
*/
|
|
382
|
+
_ref() {
|
|
383
|
+
if (this._refTimer === null) {
|
|
384
|
+
// Create a timer that keeps the event loop alive
|
|
385
|
+
this._refTimer = setInterval(() => {}, 2147483647); // Max interval
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Unref the event loop when no more pending requests
|
|
391
|
+
*/
|
|
392
|
+
_unref() {
|
|
393
|
+
if (this._pendingRequests.size === 0 && this._refTimer !== null) {
|
|
394
|
+
clearInterval(this._refTimer);
|
|
395
|
+
this._refTimer = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Ensure the callback is set up with koffi
|
|
401
|
+
*/
|
|
402
|
+
_ensureCallback() {
|
|
403
|
+
if (this._callbackPtr !== null) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Create callback function that will be invoked by Go
|
|
408
|
+
// koffi.register expects koffi.pointer(proto) as the type
|
|
409
|
+
this._callbackPtr = koffi.register((callbackId, responseJson, error) => {
|
|
410
|
+
const pending = this._pendingRequests.get(Number(callbackId));
|
|
411
|
+
if (!pending) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
this._pendingRequests.delete(Number(callbackId));
|
|
415
|
+
this._unref(); // Check if we can release the event loop
|
|
416
|
+
|
|
417
|
+
const { resolve, reject, startTime } = pending;
|
|
418
|
+
const elapsed = Date.now() - startTime;
|
|
419
|
+
|
|
420
|
+
if (error && error !== "") {
|
|
421
|
+
let errMsg = error;
|
|
422
|
+
try {
|
|
423
|
+
const errData = JSON.parse(error);
|
|
424
|
+
errMsg = errData.error || error;
|
|
425
|
+
} catch (e) {
|
|
426
|
+
// Use raw error string
|
|
427
|
+
}
|
|
428
|
+
reject(new HTTPCloakError(errMsg));
|
|
429
|
+
} else if (responseJson) {
|
|
430
|
+
try {
|
|
431
|
+
const data = JSON.parse(responseJson);
|
|
432
|
+
if (data.error) {
|
|
433
|
+
reject(new HTTPCloakError(data.error));
|
|
434
|
+
} else {
|
|
435
|
+
resolve(new Response(data, elapsed));
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
reject(new HTTPCloakError(`Failed to parse response: ${e.message}`));
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
reject(new HTTPCloakError("No response received"));
|
|
442
|
+
}
|
|
443
|
+
}, koffi.pointer(AsyncCallbackProto));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Register a new async request
|
|
448
|
+
* @returns {{ callbackId: number, promise: Promise<Response> }}
|
|
449
|
+
*/
|
|
450
|
+
registerRequest(nativeLib) {
|
|
451
|
+
this._ensureCallback();
|
|
452
|
+
|
|
453
|
+
// Register callback with Go (each request gets unique ID)
|
|
454
|
+
const callbackId = nativeLib.httpcloak_register_callback(this._callbackPtr);
|
|
455
|
+
|
|
456
|
+
// Create promise for this request with start time
|
|
457
|
+
let resolve, reject;
|
|
458
|
+
const promise = new Promise((res, rej) => {
|
|
459
|
+
resolve = res;
|
|
460
|
+
reject = rej;
|
|
461
|
+
});
|
|
462
|
+
const startTime = Date.now();
|
|
463
|
+
|
|
464
|
+
this._pendingRequests.set(Number(callbackId), { resolve, reject, startTime });
|
|
465
|
+
this._ref(); // Keep event loop alive
|
|
466
|
+
|
|
467
|
+
return { callbackId, promise };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Global async callback manager
|
|
472
|
+
let asyncManager = null;
|
|
473
|
+
|
|
474
|
+
function getAsyncManager() {
|
|
475
|
+
if (asyncManager === null) {
|
|
476
|
+
asyncManager = new AsyncCallbackManager();
|
|
477
|
+
}
|
|
478
|
+
return asyncManager;
|
|
479
|
+
}
|
|
480
|
+
|
|
244
481
|
/**
|
|
245
482
|
* Convert result to string (handles both direct strings and null)
|
|
246
483
|
* With "str" return type, koffi automatically handles the conversion
|
|
@@ -254,8 +491,11 @@ function resultToString(result) {
|
|
|
254
491
|
|
|
255
492
|
/**
|
|
256
493
|
* Parse response from the native library
|
|
494
|
+
* @param {string} resultPtr - Result pointer from native function
|
|
495
|
+
* @param {number} [elapsed=0] - Elapsed time in milliseconds
|
|
496
|
+
* @returns {Response}
|
|
257
497
|
*/
|
|
258
|
-
function parseResponse(resultPtr) {
|
|
498
|
+
function parseResponse(resultPtr, elapsed = 0) {
|
|
259
499
|
const result = resultToString(resultPtr);
|
|
260
500
|
if (!result) {
|
|
261
501
|
throw new HTTPCloakError("No response received");
|
|
@@ -267,7 +507,7 @@ function parseResponse(resultPtr) {
|
|
|
267
507
|
throw new HTTPCloakError(data.error);
|
|
268
508
|
}
|
|
269
509
|
|
|
270
|
-
return new Response(data);
|
|
510
|
+
return new Response(data, elapsed);
|
|
271
511
|
}
|
|
272
512
|
|
|
273
513
|
/**
|
|
@@ -434,7 +674,7 @@ class Session {
|
|
|
434
674
|
* Create a new session
|
|
435
675
|
* @param {Object} options - Session options
|
|
436
676
|
* @param {string} [options.preset="chrome-143"] - Browser preset to use
|
|
437
|
-
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port")
|
|
677
|
+
* @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
|
|
438
678
|
* @param {number} [options.timeout=30] - Request timeout in seconds
|
|
439
679
|
* @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
|
|
440
680
|
* @param {boolean} [options.verify=true] - SSL certificate verification
|
|
@@ -442,6 +682,9 @@ class Session {
|
|
|
442
682
|
* @param {number} [options.maxRedirects=10] - Maximum number of redirects to follow
|
|
443
683
|
* @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
|
|
444
684
|
* @param {number[]} [options.retryOnStatus] - Status codes to retry on
|
|
685
|
+
* @param {Array} [options.auth] - Default auth [username, password] for all requests
|
|
686
|
+
* @param {Object} [options.connectTo] - Domain fronting map {requestHost: connectHost}
|
|
687
|
+
* @param {string} [options.echConfigDomain] - Domain to fetch ECH config from (e.g., "cloudflare-ech.com")
|
|
445
688
|
*/
|
|
446
689
|
constructor(options = {}) {
|
|
447
690
|
const {
|
|
@@ -455,10 +698,14 @@ class Session {
|
|
|
455
698
|
retry = 3,
|
|
456
699
|
retryOnStatus = null,
|
|
457
700
|
preferIpv4 = false,
|
|
701
|
+
auth = null,
|
|
702
|
+
connectTo = null,
|
|
703
|
+
echConfigDomain = null,
|
|
458
704
|
} = options;
|
|
459
705
|
|
|
460
706
|
this._lib = getLib();
|
|
461
707
|
this.headers = {}; // Default headers
|
|
708
|
+
this.auth = auth; // Default auth for all requests
|
|
462
709
|
|
|
463
710
|
const config = {
|
|
464
711
|
preset,
|
|
@@ -484,6 +731,12 @@ class Session {
|
|
|
484
731
|
if (preferIpv4) {
|
|
485
732
|
config.prefer_ipv4 = true;
|
|
486
733
|
}
|
|
734
|
+
if (connectTo) {
|
|
735
|
+
config.connect_to = connectTo;
|
|
736
|
+
}
|
|
737
|
+
if (echConfigDomain) {
|
|
738
|
+
config.ech_config_domain = echConfigDomain;
|
|
739
|
+
}
|
|
487
740
|
|
|
488
741
|
this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
|
|
489
742
|
|
|
@@ -512,6 +765,31 @@ class Session {
|
|
|
512
765
|
return { ...this.headers, ...headers };
|
|
513
766
|
}
|
|
514
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Apply cookies to headers
|
|
770
|
+
* @param {Object} headers - Existing headers
|
|
771
|
+
* @param {Object} cookies - Cookies to apply as key-value pairs
|
|
772
|
+
* @returns {Object} Headers with cookies applied
|
|
773
|
+
*/
|
|
774
|
+
_applyCookies(headers, cookies) {
|
|
775
|
+
if (!cookies || Object.keys(cookies).length === 0) {
|
|
776
|
+
return headers;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const cookieStr = Object.entries(cookies)
|
|
780
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
781
|
+
.join("; ");
|
|
782
|
+
|
|
783
|
+
headers = headers ? { ...headers } : {};
|
|
784
|
+
const existing = headers["Cookie"] || "";
|
|
785
|
+
if (existing) {
|
|
786
|
+
headers["Cookie"] = `${existing}; ${cookieStr}`;
|
|
787
|
+
} else {
|
|
788
|
+
headers["Cookie"] = cookieStr;
|
|
789
|
+
}
|
|
790
|
+
return headers;
|
|
791
|
+
}
|
|
792
|
+
|
|
515
793
|
// ===========================================================================
|
|
516
794
|
// Synchronous Methods
|
|
517
795
|
// ===========================================================================
|
|
@@ -522,19 +800,25 @@ class Session {
|
|
|
522
800
|
* @param {Object} [options] - Request options
|
|
523
801
|
* @param {Object} [options.headers] - Custom headers
|
|
524
802
|
* @param {Object} [options.params] - Query parameters
|
|
803
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
525
804
|
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
526
805
|
* @returns {Response} Response object
|
|
527
806
|
*/
|
|
528
807
|
getSync(url, options = {}) {
|
|
529
|
-
const { headers = null, params = null, auth = null } = options;
|
|
808
|
+
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
530
809
|
|
|
531
810
|
url = addParamsToUrl(url, params);
|
|
532
811
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
533
|
-
|
|
812
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
813
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
814
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
815
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
534
816
|
|
|
535
817
|
const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
|
|
818
|
+
const startTime = Date.now();
|
|
536
819
|
const result = this._lib.httpcloak_get(this._handle, url, headersJson);
|
|
537
|
-
|
|
820
|
+
const elapsed = Date.now() - startTime;
|
|
821
|
+
return parseResponse(result, elapsed);
|
|
538
822
|
}
|
|
539
823
|
|
|
540
824
|
/**
|
|
@@ -550,11 +834,12 @@ class Session {
|
|
|
550
834
|
* - { filename, content, contentType? }: file with metadata
|
|
551
835
|
* @param {Object} [options.headers] - Custom headers
|
|
552
836
|
* @param {Object} [options.params] - Query parameters
|
|
837
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
553
838
|
* @param {Array} [options.auth] - Basic auth [username, password]
|
|
554
839
|
* @returns {Response} Response object
|
|
555
840
|
*/
|
|
556
841
|
postSync(url, options = {}) {
|
|
557
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null } = options;
|
|
842
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
558
843
|
|
|
559
844
|
url = addParamsToUrl(url, params);
|
|
560
845
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -588,11 +873,16 @@ class Session {
|
|
|
588
873
|
body = body.toString("utf8");
|
|
589
874
|
}
|
|
590
875
|
|
|
591
|
-
|
|
876
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
877
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
878
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
879
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
592
880
|
|
|
593
881
|
const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
|
|
882
|
+
const startTime = Date.now();
|
|
594
883
|
const result = this._lib.httpcloak_post(this._handle, url, body, headersJson);
|
|
595
|
-
|
|
884
|
+
const elapsed = Date.now() - startTime;
|
|
885
|
+
return parseResponse(result, elapsed);
|
|
596
886
|
}
|
|
597
887
|
|
|
598
888
|
/**
|
|
@@ -600,11 +890,12 @@ class Session {
|
|
|
600
890
|
* @param {string} method - HTTP method
|
|
601
891
|
* @param {string} url - Request URL
|
|
602
892
|
* @param {Object} [options] - Request options
|
|
893
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
603
894
|
* @param {Object} [options.files] - Files to upload as multipart/form-data
|
|
604
895
|
* @returns {Response} Response object
|
|
605
896
|
*/
|
|
606
897
|
requestSync(method, url, options = {}) {
|
|
607
|
-
let { body = null, json = null, data = null, files = null, headers = null, params = null, auth = null, timeout = null } = options;
|
|
898
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
|
|
608
899
|
|
|
609
900
|
url = addParamsToUrl(url, params);
|
|
610
901
|
let mergedHeaders = this._mergeHeaders(headers);
|
|
@@ -638,7 +929,10 @@ class Session {
|
|
|
638
929
|
body = body.toString("utf8");
|
|
639
930
|
}
|
|
640
931
|
|
|
641
|
-
|
|
932
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
933
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
934
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
935
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
642
936
|
|
|
643
937
|
const requestConfig = {
|
|
644
938
|
method: method.toUpperCase(),
|
|
@@ -648,70 +942,171 @@ class Session {
|
|
|
648
942
|
if (body) requestConfig.body = body;
|
|
649
943
|
if (timeout) requestConfig.timeout = timeout;
|
|
650
944
|
|
|
945
|
+
const startTime = Date.now();
|
|
651
946
|
const result = this._lib.httpcloak_request(
|
|
652
947
|
this._handle,
|
|
653
948
|
JSON.stringify(requestConfig)
|
|
654
949
|
);
|
|
655
|
-
|
|
950
|
+
const elapsed = Date.now() - startTime;
|
|
951
|
+
return parseResponse(result, elapsed);
|
|
656
952
|
}
|
|
657
953
|
|
|
658
954
|
// ===========================================================================
|
|
659
|
-
// Promise-based Methods
|
|
955
|
+
// Promise-based Methods (Native async using Go goroutines)
|
|
660
956
|
// ===========================================================================
|
|
661
957
|
|
|
662
958
|
/**
|
|
663
|
-
* Perform an async GET request
|
|
959
|
+
* Perform an async GET request using native Go goroutines
|
|
664
960
|
* @param {string} url - Request URL
|
|
665
961
|
* @param {Object} [options] - Request options
|
|
962
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
666
963
|
* @returns {Promise<Response>} Response object
|
|
667
964
|
*/
|
|
668
965
|
get(url, options = {}) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
966
|
+
const { headers = null, params = null, cookies = null, auth = null } = options;
|
|
967
|
+
|
|
968
|
+
url = addParamsToUrl(url, params);
|
|
969
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
970
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
971
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
972
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
973
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
974
|
+
|
|
975
|
+
const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
|
|
976
|
+
|
|
977
|
+
// Register async request with callback manager
|
|
978
|
+
const manager = getAsyncManager();
|
|
979
|
+
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
980
|
+
|
|
981
|
+
// Start async request
|
|
982
|
+
this._lib.httpcloak_get_async(this._handle, url, headersJson, callbackId);
|
|
983
|
+
|
|
984
|
+
return promise;
|
|
678
985
|
}
|
|
679
986
|
|
|
680
987
|
/**
|
|
681
|
-
* Perform an async POST request
|
|
988
|
+
* Perform an async POST request using native Go goroutines
|
|
682
989
|
* @param {string} url - Request URL
|
|
683
990
|
* @param {Object} [options] - Request options
|
|
991
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
684
992
|
* @returns {Promise<Response>} Response object
|
|
685
993
|
*/
|
|
686
994
|
post(url, options = {}) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
995
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null } = options;
|
|
996
|
+
|
|
997
|
+
url = addParamsToUrl(url, params);
|
|
998
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
999
|
+
|
|
1000
|
+
// Handle multipart file upload
|
|
1001
|
+
if (files !== null) {
|
|
1002
|
+
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1003
|
+
const multipart = encodeMultipart(formData, files);
|
|
1004
|
+
body = multipart.body.toString("latin1");
|
|
1005
|
+
mergedHeaders = mergedHeaders || {};
|
|
1006
|
+
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1007
|
+
}
|
|
1008
|
+
// Handle JSON body
|
|
1009
|
+
else if (json !== null) {
|
|
1010
|
+
body = JSON.stringify(json);
|
|
1011
|
+
mergedHeaders = mergedHeaders || {};
|
|
1012
|
+
if (!mergedHeaders["Content-Type"]) {
|
|
1013
|
+
mergedHeaders["Content-Type"] = "application/json";
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// Handle form data
|
|
1017
|
+
else if (data !== null && typeof data === "object") {
|
|
1018
|
+
body = new URLSearchParams(data).toString();
|
|
1019
|
+
mergedHeaders = mergedHeaders || {};
|
|
1020
|
+
if (!mergedHeaders["Content-Type"]) {
|
|
1021
|
+
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// Handle Buffer body
|
|
1025
|
+
else if (Buffer.isBuffer(body)) {
|
|
1026
|
+
body = body.toString("utf8");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1030
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1031
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1032
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1033
|
+
|
|
1034
|
+
const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
|
|
1035
|
+
|
|
1036
|
+
// Register async request with callback manager
|
|
1037
|
+
const manager = getAsyncManager();
|
|
1038
|
+
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1039
|
+
|
|
1040
|
+
// Start async request
|
|
1041
|
+
this._lib.httpcloak_post_async(this._handle, url, body, headersJson, callbackId);
|
|
1042
|
+
|
|
1043
|
+
return promise;
|
|
696
1044
|
}
|
|
697
1045
|
|
|
698
1046
|
/**
|
|
699
|
-
* Perform an async custom HTTP request
|
|
1047
|
+
* Perform an async custom HTTP request using native Go goroutines
|
|
700
1048
|
* @param {string} method - HTTP method
|
|
701
1049
|
* @param {string} url - Request URL
|
|
702
1050
|
* @param {Object} [options] - Request options
|
|
1051
|
+
* @param {Object} [options.cookies] - Cookies to send with this request
|
|
703
1052
|
* @returns {Promise<Response>} Response object
|
|
704
1053
|
*/
|
|
705
1054
|
request(method, url, options = {}) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1055
|
+
let { body = null, json = null, data = null, files = null, headers = null, params = null, cookies = null, auth = null, timeout = null } = options;
|
|
1056
|
+
|
|
1057
|
+
url = addParamsToUrl(url, params);
|
|
1058
|
+
let mergedHeaders = this._mergeHeaders(headers);
|
|
1059
|
+
|
|
1060
|
+
// Handle multipart file upload
|
|
1061
|
+
if (files !== null) {
|
|
1062
|
+
const formData = (data !== null && typeof data === "object") ? data : null;
|
|
1063
|
+
const multipart = encodeMultipart(formData, files);
|
|
1064
|
+
body = multipart.body.toString("latin1");
|
|
1065
|
+
mergedHeaders = mergedHeaders || {};
|
|
1066
|
+
mergedHeaders["Content-Type"] = multipart.contentType;
|
|
1067
|
+
}
|
|
1068
|
+
// Handle JSON body
|
|
1069
|
+
else if (json !== null) {
|
|
1070
|
+
body = JSON.stringify(json);
|
|
1071
|
+
mergedHeaders = mergedHeaders || {};
|
|
1072
|
+
if (!mergedHeaders["Content-Type"]) {
|
|
1073
|
+
mergedHeaders["Content-Type"] = "application/json";
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// Handle form data
|
|
1077
|
+
else if (data !== null && typeof data === "object") {
|
|
1078
|
+
body = new URLSearchParams(data).toString();
|
|
1079
|
+
mergedHeaders = mergedHeaders || {};
|
|
1080
|
+
if (!mergedHeaders["Content-Type"]) {
|
|
1081
|
+
mergedHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
// Handle Buffer body
|
|
1085
|
+
else if (Buffer.isBuffer(body)) {
|
|
1086
|
+
body = body.toString("utf8");
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Use request auth if provided, otherwise fall back to session auth
|
|
1090
|
+
const effectiveAuth = auth !== null ? auth : this.auth;
|
|
1091
|
+
mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
|
|
1092
|
+
mergedHeaders = this._applyCookies(mergedHeaders, cookies);
|
|
1093
|
+
|
|
1094
|
+
const requestConfig = {
|
|
1095
|
+
method: method.toUpperCase(),
|
|
1096
|
+
url,
|
|
1097
|
+
};
|
|
1098
|
+
if (mergedHeaders) requestConfig.headers = mergedHeaders;
|
|
1099
|
+
if (body) requestConfig.body = body;
|
|
1100
|
+
if (timeout) requestConfig.timeout = timeout;
|
|
1101
|
+
|
|
1102
|
+
// Register async request with callback manager
|
|
1103
|
+
const manager = getAsyncManager();
|
|
1104
|
+
const { callbackId, promise } = manager.registerRequest(this._lib);
|
|
1105
|
+
|
|
1106
|
+
// Start async request
|
|
1107
|
+
this._lib.httpcloak_request_async(this._handle, JSON.stringify(requestConfig), callbackId);
|
|
1108
|
+
|
|
1109
|
+
return promise;
|
|
715
1110
|
}
|
|
716
1111
|
|
|
717
1112
|
/**
|
|
@@ -766,6 +1161,16 @@ class Session {
|
|
|
766
1161
|
return {};
|
|
767
1162
|
}
|
|
768
1163
|
|
|
1164
|
+
/**
|
|
1165
|
+
* Get a specific cookie by name
|
|
1166
|
+
* @param {string} name - Cookie name
|
|
1167
|
+
* @returns {string|null} Cookie value or null if not found
|
|
1168
|
+
*/
|
|
1169
|
+
getCookie(name) {
|
|
1170
|
+
const cookies = this.getCookies();
|
|
1171
|
+
return cookies[name] || null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
769
1174
|
/**
|
|
770
1175
|
* Set a cookie in the session
|
|
771
1176
|
* @param {string} name - Cookie name
|
|
@@ -775,6 +1180,25 @@ class Session {
|
|
|
775
1180
|
this._lib.httpcloak_set_cookie(this._handle, name, value);
|
|
776
1181
|
}
|
|
777
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* Delete a specific cookie by name
|
|
1185
|
+
* @param {string} name - Cookie name to delete
|
|
1186
|
+
*/
|
|
1187
|
+
deleteCookie(name) {
|
|
1188
|
+
// Set cookie to empty value - effectively deletes it
|
|
1189
|
+
this._lib.httpcloak_set_cookie(this._handle, name, "");
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Clear all cookies from the session
|
|
1194
|
+
*/
|
|
1195
|
+
clearCookies() {
|
|
1196
|
+
const cookies = this.getCookies();
|
|
1197
|
+
for (const name of Object.keys(cookies)) {
|
|
1198
|
+
this.deleteCookie(name);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
778
1202
|
/**
|
|
779
1203
|
* Get cookies as a property
|
|
780
1204
|
*/
|
|
@@ -960,6 +1384,8 @@ module.exports = {
|
|
|
960
1384
|
// Classes
|
|
961
1385
|
Session,
|
|
962
1386
|
Response,
|
|
1387
|
+
Cookie,
|
|
1388
|
+
RedirectInfo,
|
|
963
1389
|
HTTPCloakError,
|
|
964
1390
|
// Presets
|
|
965
1391
|
Preset,
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTPCloak Node.js Client - ESM Module
|
|
3
|
+
*
|
|
4
|
+
* A fetch/axios-compatible HTTP client with browser fingerprint emulation.
|
|
5
|
+
* Provides TLS fingerprinting for HTTP requests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
// Import the CommonJS module
|
|
12
|
+
const cjs = require("./index.js");
|
|
13
|
+
|
|
14
|
+
// Re-export all named exports
|
|
15
|
+
export const Session = cjs.Session;
|
|
16
|
+
export const Response = cjs.Response;
|
|
17
|
+
export const HTTPCloakError = cjs.HTTPCloakError;
|
|
18
|
+
export const Preset = cjs.Preset;
|
|
19
|
+
export const configure = cjs.configure;
|
|
20
|
+
export const get = cjs.get;
|
|
21
|
+
export const post = cjs.post;
|
|
22
|
+
export const put = cjs.put;
|
|
23
|
+
export const patch = cjs.patch;
|
|
24
|
+
export const head = cjs.head;
|
|
25
|
+
export const options = cjs.options;
|
|
26
|
+
export const request = cjs.request;
|
|
27
|
+
export const version = cjs.version;
|
|
28
|
+
export const availablePresets = cjs.availablePresets;
|
|
29
|
+
|
|
30
|
+
// 'delete' is a reserved word in ESM, so we export it specially
|
|
31
|
+
const del = cjs.delete;
|
|
32
|
+
export { del as delete };
|
|
33
|
+
|
|
34
|
+
// Default export (the entire module)
|
|
35
|
+
export default cjs;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@httpcloak/darwin-arm64",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "HTTPCloak native binary for darwin arm64",
|
|
5
5
|
"os": [
|
|
6
6
|
"darwin"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"arm64"
|
|
10
10
|
],
|
|
11
11
|
"main": "lib.js",
|
|
12
|
+
"module": "lib.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./lib.mjs",
|
|
16
|
+
"require": "./lib.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"license": "MIT",
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@httpcloak/darwin-x64",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "HTTPCloak native binary for darwin x64",
|
|
5
5
|
"os": [
|
|
6
6
|
"darwin"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"x64"
|
|
10
10
|
],
|
|
11
11
|
"main": "lib.js",
|
|
12
|
+
"module": "lib.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./lib.mjs",
|
|
16
|
+
"require": "./lib.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"license": "MIT",
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@httpcloak/linux-arm64",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "HTTPCloak native binary for linux arm64",
|
|
5
5
|
"os": [
|
|
6
6
|
"linux"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"arm64"
|
|
10
10
|
],
|
|
11
11
|
"main": "lib.js",
|
|
12
|
+
"module": "lib.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./lib.mjs",
|
|
16
|
+
"require": "./lib.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"license": "MIT",
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@httpcloak/linux-x64",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "HTTPCloak native binary for linux x64",
|
|
5
5
|
"os": [
|
|
6
6
|
"linux"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"x64"
|
|
10
10
|
],
|
|
11
11
|
"main": "lib.js",
|
|
12
|
+
"module": "lib.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./lib.mjs",
|
|
16
|
+
"require": "./lib.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"license": "MIT",
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@httpcloak/win32-arm64",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "HTTPCloak native binary for win32 arm64",
|
|
5
5
|
"os": [
|
|
6
6
|
"win32"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"arm64"
|
|
10
10
|
],
|
|
11
11
|
"main": "lib.js",
|
|
12
|
+
"module": "lib.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./lib.mjs",
|
|
16
|
+
"require": "./lib.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"license": "MIT",
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@httpcloak/win32-x64",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "HTTPCloak native binary for win32 x64",
|
|
5
5
|
"os": [
|
|
6
6
|
"win32"
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"x64"
|
|
10
10
|
],
|
|
11
11
|
"main": "lib.js",
|
|
12
|
+
"module": "lib.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./lib.mjs",
|
|
16
|
+
"require": "./lib.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"license": "MIT",
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
package/package.json
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "httpcloak",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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
|
+
"module": "lib/index.mjs",
|
|
6
7
|
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"default": "./lib/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./lib/index.d.ts",
|
|
16
|
+
"default": "./lib/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
7
20
|
"scripts": {
|
|
8
21
|
"test": "node test.js",
|
|
22
|
+
"test:esm": "node test.mjs",
|
|
9
23
|
"setup-packages": "node scripts/setup-npm-packages.js"
|
|
10
24
|
},
|
|
11
25
|
"keywords": [
|
|
@@ -35,11 +49,11 @@
|
|
|
35
49
|
"koffi": "^2.9.0"
|
|
36
50
|
},
|
|
37
51
|
"optionalDependencies": {
|
|
38
|
-
"@httpcloak/linux-x64": "1.5.
|
|
39
|
-
"@httpcloak/linux-arm64": "1.5.
|
|
40
|
-
"@httpcloak/darwin-x64": "1.5.
|
|
41
|
-
"@httpcloak/darwin-arm64": "1.5.
|
|
42
|
-
"@httpcloak/win32-x64": "1.5.
|
|
43
|
-
"@httpcloak/win32-arm64": "1.5.
|
|
52
|
+
"@httpcloak/linux-x64": "1.5.1",
|
|
53
|
+
"@httpcloak/linux-arm64": "1.5.1",
|
|
54
|
+
"@httpcloak/darwin-x64": "1.5.1",
|
|
55
|
+
"@httpcloak/darwin-arm64": "1.5.1",
|
|
56
|
+
"@httpcloak/win32-x64": "1.5.1",
|
|
57
|
+
"@httpcloak/win32-arm64": "1.5.1"
|
|
44
58
|
}
|
|
45
59
|
}
|