stealth-ws 1.0.0
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/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/stealth-bridge.exe +0 -0
- package/lib/cookie-jar.js +276 -0
- package/lib/fingerprint.js +208 -0
- package/lib/index.d.ts +74 -0
- package/lib/index.js +29 -0
- package/lib/websocket.js +416 -0
- package/package.json +58 -0
- package/prebuilds/darwin-x64/stealth-bridge +0 -0
- package/prebuilds/linux-x64/stealth-bridge +0 -0
- package/prebuilds/win32-x64/stealth-bridge.exe +0 -0
- package/scripts/build-all.js +102 -0
- package/scripts/build-go.js +134 -0
- package/scripts/download-binary.js +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# stealth-ws
|
|
2
|
+
|
|
3
|
+
WebSocket client with TLS fingerprint spoofing. Produces TLS `ClientHello` messages that are byte-for-byte identical to real browser handshakes, bypassing fingerprint-based bot detection at the transport layer.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How TLS Fingerprinting Works
|
|
8
|
+
|
|
9
|
+
When any TLS client connects to a server, the first thing it sends is a `ClientHello` message. This message contains:
|
|
10
|
+
|
|
11
|
+
- **Cipher suites** — list of encryption algorithms the client supports, in preference order
|
|
12
|
+
- **TLS extensions** — e.g. `server_name` (SNI), `supported_groups`, `signature_algorithms`, `key_share`, `session_ticket`, `ALPN`, etc.
|
|
13
|
+
- **Extension ordering** — the exact sequence in which extensions appear
|
|
14
|
+
- **Compression methods**
|
|
15
|
+
- **Supported TLS versions**
|
|
16
|
+
|
|
17
|
+
Every TLS implementation has a unique combination of these fields. Chrome, Firefox, curl, Node.js's built-in `https`, Go's `net/tls` — they all produce structurally different `ClientHello` messages.
|
|
18
|
+
|
|
19
|
+
### JA3
|
|
20
|
+
|
|
21
|
+
JA3 (Salesforce, 2017) is the first widely adopted fingerprinting method. It computes an MD5 hash over five fields extracted from the `ClientHello`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
JA3 = MD5(TLSVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each field is a comma-separated list of decimal values, joined by `|`. For example:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
771,4866-4867-4865-...,0-23-65281-10-11-35-16-5-13-...,29-23-24,0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This string is MD5-hashed to produce a 32-character fingerprint like `cd08e31494f9531f560d64c695473da9` (Chrome 120).
|
|
34
|
+
|
|
35
|
+
Because the hash is deterministic and stable per browser version, servers can maintain a database of known-good hashes (Chrome, Firefox, Safari) and reject anything that doesn't match — including raw TLS stacks like Go's `crypto/tls`, Node.js's `tls`, or Python's `ssl`.
|
|
36
|
+
|
|
37
|
+
### JA3S
|
|
38
|
+
|
|
39
|
+
The server-side counterpart. Hashes fields from the `ServerHello` response. Used to fingerprint servers, less commonly used for bot detection.
|
|
40
|
+
|
|
41
|
+
### JA4
|
|
42
|
+
|
|
43
|
+
JA4 (FoxIO, 2023) is a successor to JA3 that addresses several weaknesses:
|
|
44
|
+
|
|
45
|
+
- JA3 is broken by **extension randomization** (Chrome 110+ shuffles extension order) — the same browser produces different JA3 hashes across connections
|
|
46
|
+
- JA4 sorts extensions and cipher suites before hashing, making it **randomization-resistant**
|
|
47
|
+
- JA4 uses a human-readable format (`t13d1516h2_...`) instead of MD5, making it inspectable without a lookup table
|
|
48
|
+
- JA4 also captures ALPN first/last values and whether SNI is present
|
|
49
|
+
|
|
50
|
+
JA4 format:
|
|
51
|
+
```
|
|
52
|
+
{protocol}{tls_version}{sni}{cipher_count}{ext_count}{alpn_first_last}_{sorted_cipher_hash}_{sorted_ext_hash}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example: `t13d1516h2_8daaf6152771_b0da82dd1658` (Chrome 120)
|
|
56
|
+
|
|
57
|
+
### What Gets Inspected in Practice
|
|
58
|
+
|
|
59
|
+
Bot detection services (Cloudflare, Akamai, DataDome, PerimeterX, etc.) don't just check JA3/JA4. A full fingerprint inspection layer includes:
|
|
60
|
+
|
|
61
|
+
| Layer | What's checked |
|
|
62
|
+
|-------|----------------|
|
|
63
|
+
| TLS | JA3, JA4, cipher suite order, extension order, `key_share` groups, ALPN |
|
|
64
|
+
| HTTP/1.1 | Header order, `User-Agent`, `Accept`, `Accept-Encoding`, `Accept-Language` |
|
|
65
|
+
| HTTP/2 | SETTINGS frame values, WINDOW_UPDATE size, header pseudo-order, HPACK huffman encoding |
|
|
66
|
+
| WebSocket | Upgrade header casing, extension negotiation |
|
|
67
|
+
| Behavioral | Request timing, mouse movement, JS challenge results |
|
|
68
|
+
|
|
69
|
+
A Node.js WebSocket client using the standard `ws` package over Node's built-in TLS will fail at the first layer — the TLS fingerprint — before any HTTP headers are even examined, because Go's and Node's `crypto/tls` produce well-known non-browser fingerprints that are trivially blocklisted.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What This Package Does
|
|
74
|
+
|
|
75
|
+
### The Problem
|
|
76
|
+
|
|
77
|
+
Node.js's `tls` module uses Go's `crypto/tls` under the hood (via libuv/OpenSSL). Its `ClientHello` looks nothing like a browser. The cipher suite list, extension set, and ordering are all wrong. You can set `User-Agent: Mozilla/5.0 ...` all you want — the TLS handshake happens before HTTP and gives you away immediately.
|
|
78
|
+
|
|
79
|
+
### The Solution
|
|
80
|
+
|
|
81
|
+
This package spawns a Go subprocess (`stealth-bridge`) that uses [**uTLS**](https://github.com/refraction-networking/utls) — a fork of Go's `crypto/tls` that allows full manual control over `ClientHello` construction. uTLS ships pre-built `ClientHelloSpec` definitions for every major browser version, capturing the exact cipher suites, extensions, values, and ordering that real browsers produce.
|
|
82
|
+
|
|
83
|
+
The Go bridge:
|
|
84
|
+
1. Receives connection config from Node.js over stdin (JSON)
|
|
85
|
+
2. Dials the target with a uTLS connection using the specified browser spec
|
|
86
|
+
3. Performs the WebSocket upgrade over the spoofed TLS connection
|
|
87
|
+
4. Relays frames to/from Node.js over stdout/stdin (newline-delimited JSON)
|
|
88
|
+
|
|
89
|
+
Node.js never touches the TLS connection directly. The handshake is entirely owned by the Go process.
|
|
90
|
+
|
|
91
|
+
### ALPN Handling
|
|
92
|
+
|
|
93
|
+
WebSocket connections use HTTP/1.1 for the upgrade handshake. Chrome sends `ALPN: http/1.1` for WebSocket connections — not `h2`. If the server negotiates HTTP/2 in response to an `h2` ALPN offer, the WebSocket upgrade will fail because HTTP/2 doesn't support the `Connection: Upgrade` mechanism (RFC 7540 §8.1.1).
|
|
94
|
+
|
|
95
|
+
The bridge explicitly patches the ALPN extension in the cloned spec before the handshake:
|
|
96
|
+
|
|
97
|
+
```go
|
|
98
|
+
alpn.AlpnProtocols = []string{"http/1.1"}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This ensures the TLS fingerprint is still browser-accurate while forcing the connection to HTTP/1.1 for the WebSocket upgrade.
|
|
102
|
+
|
|
103
|
+
### Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Node.js process
|
|
107
|
+
│
|
|
108
|
+
├── new WebSocket(url, options)
|
|
109
|
+
│ └── spawn stealth-bridge (Go binary)
|
|
110
|
+
│ │
|
|
111
|
+
│ ├── stdin ← JSON config + send/close commands
|
|
112
|
+
│ └── stdout → JSON events (open/message/binary/close/error)
|
|
113
|
+
│
|
|
114
|
+
└── stealth-bridge
|
|
115
|
+
├── uTLS ClientHello (spoofed browser spec)
|
|
116
|
+
├── TCP → TLS → HTTP/1.1 Upgrade → WebSocket
|
|
117
|
+
└── gorilla/websocket for frame handling
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Available Fingerprint Profiles
|
|
121
|
+
|
|
122
|
+
Profiles map to pre-built `ClientHelloSpec` definitions in uTLS v1.6.6:
|
|
123
|
+
|
|
124
|
+
| Profile | Maps to |
|
|
125
|
+
|---------|---------|
|
|
126
|
+
| `chrome120` | `HelloChrome_120` |
|
|
127
|
+
| `chrome115` | `HelloChrome_115_PQ` (post-quantum key share) |
|
|
128
|
+
| `chrome114` | `HelloChrome_114_Padding_PSK_Shuf` |
|
|
129
|
+
| `chrome112` | `HelloChrome_112_PSK_Shuf` |
|
|
130
|
+
| `chrome100` | `HelloChrome_100` |
|
|
131
|
+
| `chromeAuto` | `HelloChrome_Auto` (latest Chrome in uTLS) |
|
|
132
|
+
| `firefox120` | `HelloFirefox_120` |
|
|
133
|
+
| `firefoxAuto` | `HelloFirefox_Auto` |
|
|
134
|
+
| `safari16` | `HelloSafari_16_0` |
|
|
135
|
+
| `safariAuto` | `HelloSafari_Auto` |
|
|
136
|
+
| `edge106` | `HelloEdge_106` |
|
|
137
|
+
| `edgeAuto` | `HelloEdge_Auto` |
|
|
138
|
+
| `ios14` | `HelloIOS_14` |
|
|
139
|
+
| `iosAuto` | `HelloIOS_Auto` |
|
|
140
|
+
| `android11` | `HelloAndroid_11_OkHttp` |
|
|
141
|
+
|
|
142
|
+
> Note: uTLS v1.6.6 does not include specs for every minor browser version. Profiles without an exact match fall back to the nearest available spec. See `fingerprintMap` in `src/bridge/main.go` for the full mapping.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Installation
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install stealth-ws
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The `postinstall` script copies the prebuilt binary for your platform from `prebuilds/{platform}-x64/` to `bin/`.
|
|
153
|
+
|
|
154
|
+
## Quick Start
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
import WebSocket from 'stealth-ws';
|
|
158
|
+
|
|
159
|
+
const ws = new WebSocket('wss://example.com/ws', {
|
|
160
|
+
fingerprint: 'chrome120',
|
|
161
|
+
headers: {
|
|
162
|
+
'Origin': 'https://example.com',
|
|
163
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
ws.on('open', () => {
|
|
168
|
+
ws.send('Hello!');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ws.on('message', (data, isBinary) => {
|
|
172
|
+
console.log('Received:', data.toString());
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## API
|
|
177
|
+
|
|
178
|
+
### `new WebSocket(url, options)`
|
|
179
|
+
|
|
180
|
+
| Option | Type | Default | Description |
|
|
181
|
+
|--------|------|---------|-------------|
|
|
182
|
+
| `fingerprint` | `string` | `'chrome120'` | TLS fingerprint profile |
|
|
183
|
+
| `cookies` | `string\|Array\|CookieJar` | — | Cookie header value |
|
|
184
|
+
| `proxy` | `string` | — | `socks5://` or `http://` proxy URL |
|
|
185
|
+
| `headers` | `object` | — | HTTP headers sent on upgrade (Origin, User-Agent, etc.) |
|
|
186
|
+
| `perMessageDeflate` | `boolean` | `true` | Enable per-message deflate compression |
|
|
187
|
+
| `debug` | `boolean` | `false` | Log debug messages from bridge |
|
|
188
|
+
|
|
189
|
+
### Events
|
|
190
|
+
|
|
191
|
+
| Event | Args | Description |
|
|
192
|
+
|-------|------|-------------|
|
|
193
|
+
| `open` | — | Handshake complete |
|
|
194
|
+
| `message` | `(data: Buffer, isBinary: boolean)` | Frame received |
|
|
195
|
+
| `close` | `(code: number, reason: string)` | Connection closed |
|
|
196
|
+
| `error` | `(err: Error)` | Error |
|
|
197
|
+
| `auth_required` | — | Server returned 403 |
|
|
198
|
+
|
|
199
|
+
### Methods
|
|
200
|
+
|
|
201
|
+
| Method | Description |
|
|
202
|
+
|--------|-------------|
|
|
203
|
+
| `send(data, [options], [cb])` | Send text or binary frame |
|
|
204
|
+
| `close([code], [reason])` | Graceful close |
|
|
205
|
+
| `terminate()` | Kill bridge process immediately |
|
|
206
|
+
| `ping([data], [mask], [cb])` | Send ping frame |
|
|
207
|
+
| `pong([data], [mask], [cb])` | Send pong frame |
|
|
208
|
+
|
|
209
|
+
### Cookie Management
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// String
|
|
213
|
+
new WebSocket(url, { cookies: 'session=abc; token=xyz' });
|
|
214
|
+
|
|
215
|
+
// Array (Puppeteer format)
|
|
216
|
+
const cookies = await page.cookies();
|
|
217
|
+
new WebSocket(url, { cookies });
|
|
218
|
+
|
|
219
|
+
// CookieJar
|
|
220
|
+
import { CookieJar } from 'stealth-ws';
|
|
221
|
+
const jar = new CookieJar();
|
|
222
|
+
jar.loadFromFile('cookies.json');
|
|
223
|
+
new WebSocket(url, { cookies: jar });
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Proxy
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
new WebSocket(url, { proxy: 'socks5://127.0.0.1:1080', fingerprint: 'chrome120' });
|
|
230
|
+
new WebSocket(url, { proxy: 'http://user:pass@proxy.example.com:8080' });
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Platform Support
|
|
234
|
+
|
|
235
|
+
| Platform | Status |
|
|
236
|
+
|----------|--------|
|
|
237
|
+
| Windows x64 | ✅ prebuilt |
|
|
238
|
+
| Linux x64 | ✅ prebuilt |
|
|
239
|
+
| macOS x64 | ✅ prebuilt |
|
|
240
|
+
| Any ARM64 | ❌ build from source: `npm run build:go` |
|
|
241
|
+
|
|
242
|
+
## Building from Source
|
|
243
|
+
|
|
244
|
+
Requires Go 1.21+.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Current platform only
|
|
248
|
+
npm run build:go
|
|
249
|
+
|
|
250
|
+
# All x64 platforms (cross-compile)
|
|
251
|
+
npm run prebuild
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Migration from `ws`
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
// Before
|
|
258
|
+
import WebSocket from 'ws';
|
|
259
|
+
const ws = new WebSocket('wss://example.com');
|
|
260
|
+
|
|
261
|
+
// After
|
|
262
|
+
import WebSocket from 'stealth-ws';
|
|
263
|
+
const ws = new WebSocket('wss://example.com', {
|
|
264
|
+
fingerprint: 'chrome120',
|
|
265
|
+
headers: { 'Origin': 'https://example.com' }
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The API is compatible. Add `fingerprint` and `headers` as needed.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CookieJar - Cookie management for WebSocket connections
|
|
3
|
+
*
|
|
4
|
+
* Provides cookie storage and retrieval similar to browser CookieJar.
|
|
5
|
+
* Can load cookies from Puppeteer or other sources.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
+
import { parse, serialize } from 'url';
|
|
10
|
+
|
|
11
|
+
export class CookieJar {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new CookieJar
|
|
14
|
+
*/
|
|
15
|
+
constructor() {
|
|
16
|
+
this.cookies = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set a cookie
|
|
21
|
+
*
|
|
22
|
+
* @param {string} name - Cookie name
|
|
23
|
+
* @param {string} value - Cookie value
|
|
24
|
+
* @param {Object} options - Cookie options
|
|
25
|
+
* @param {string} [options.domain] - Cookie domain
|
|
26
|
+
* @param {string} [options.path] - Cookie path
|
|
27
|
+
* @param {Date} [options.expires] - Expiration date
|
|
28
|
+
* @param {number} [options.maxAge] - Max age in seconds
|
|
29
|
+
* @param {boolean} [options.secure] - Secure flag
|
|
30
|
+
* @param {boolean} [options.httpOnly] - HttpOnly flag
|
|
31
|
+
* @param {string} [options.sameSite] - SameSite attribute
|
|
32
|
+
*/
|
|
33
|
+
set(name, value, options = {}) {
|
|
34
|
+
const cookie = {
|
|
35
|
+
name,
|
|
36
|
+
value,
|
|
37
|
+
domain: options.domain || '',
|
|
38
|
+
path: options.path || '/',
|
|
39
|
+
expires: options.expires || null,
|
|
40
|
+
maxAge: options.maxAge || null,
|
|
41
|
+
secure: options.secure || false,
|
|
42
|
+
httpOnly: options.httpOnly || false,
|
|
43
|
+
sameSite: options.sameSite || null
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const key = this._makeKey(cookie);
|
|
47
|
+
this.cookies.set(key, cookie);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get a cookie by name for a URL
|
|
52
|
+
*
|
|
53
|
+
* @param {string} name - Cookie name
|
|
54
|
+
* @param {string} url - URL to get cookie for
|
|
55
|
+
* @returns {string|null} Cookie value or null
|
|
56
|
+
*/
|
|
57
|
+
get(name, url) {
|
|
58
|
+
const cookie = this._findCookie(name, url);
|
|
59
|
+
return cookie ? cookie.value : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all cookies for a URL
|
|
64
|
+
*
|
|
65
|
+
* @param {string} url - URL to get cookies for
|
|
66
|
+
* @returns {Array} Array of cookie objects
|
|
67
|
+
*/
|
|
68
|
+
getCookies(url) {
|
|
69
|
+
const parsedUrl = parse(url);
|
|
70
|
+
const domain = parsedUrl.hostname;
|
|
71
|
+
const path = parsedUrl.pathname;
|
|
72
|
+
const isSecure = parsedUrl.protocol === 'https:';
|
|
73
|
+
|
|
74
|
+
const result = [];
|
|
75
|
+
|
|
76
|
+
for (const cookie of this.cookies.values()) {
|
|
77
|
+
if (this._matchesCookie(cookie, domain, path, isSecure)) {
|
|
78
|
+
result.push({ ...cookie });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get cookies as a string for a URL
|
|
87
|
+
*
|
|
88
|
+
* @param {string} url - URL to get cookies for
|
|
89
|
+
* @returns {string} Cookie string
|
|
90
|
+
*/
|
|
91
|
+
getCookieString(url) {
|
|
92
|
+
const cookies = this.getCookies(url);
|
|
93
|
+
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a cookie exists
|
|
98
|
+
*
|
|
99
|
+
* @param {string} name - Cookie name
|
|
100
|
+
* @param {string} url - URL to check
|
|
101
|
+
* @returns {boolean} True if exists
|
|
102
|
+
*/
|
|
103
|
+
has(name, url) {
|
|
104
|
+
return this._findCookie(name, url) !== null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove a cookie
|
|
109
|
+
*
|
|
110
|
+
* @param {string} name - Cookie name
|
|
111
|
+
* @param {string} url - URL
|
|
112
|
+
*/
|
|
113
|
+
remove(name, url) {
|
|
114
|
+
const cookie = this._findCookie(name, url);
|
|
115
|
+
if (cookie) {
|
|
116
|
+
const key = this._makeKey(cookie);
|
|
117
|
+
this.cookies.delete(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clear all cookies
|
|
123
|
+
*/
|
|
124
|
+
clear() {
|
|
125
|
+
this.cookies.clear();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load cookies from a Puppeteer cookies array
|
|
130
|
+
*
|
|
131
|
+
* @param {Array} cookies - Puppeteer cookies
|
|
132
|
+
*/
|
|
133
|
+
loadFromPuppeteer(cookies) {
|
|
134
|
+
for (const cookie of cookies) {
|
|
135
|
+
this.set(cookie.name, cookie.value, {
|
|
136
|
+
domain: cookie.domain,
|
|
137
|
+
path: cookie.path,
|
|
138
|
+
expires: cookie.expires ? new Date(cookie.expires * 1000) : null,
|
|
139
|
+
maxAge: cookie.maxAge,
|
|
140
|
+
secure: cookie.secure,
|
|
141
|
+
httpOnly: cookie.httpOnly,
|
|
142
|
+
sameSite: cookie.sameSite
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load cookies from a JSON file
|
|
149
|
+
*
|
|
150
|
+
* @param {string} filename - Path to JSON file
|
|
151
|
+
*/
|
|
152
|
+
loadFromFile(filename) {
|
|
153
|
+
if (!existsSync(filename)) {
|
|
154
|
+
throw new Error(`Cookie file not found: ${filename}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = JSON.parse(readFileSync(filename, 'utf8'));
|
|
158
|
+
|
|
159
|
+
// Handle array format (Puppeteer style)
|
|
160
|
+
if (Array.isArray(data)) {
|
|
161
|
+
this.loadFromPuppeteer(data);
|
|
162
|
+
} else if (data.cookies && Array.isArray(data.cookies)) {
|
|
163
|
+
this.loadFromPuppeteer(data.cookies);
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error('Invalid cookie file format');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Save cookies to a JSON file
|
|
171
|
+
*
|
|
172
|
+
* @param {string} filename - Path to save to
|
|
173
|
+
*/
|
|
174
|
+
saveToFile(filename) {
|
|
175
|
+
const cookies = Array.from(this.cookies.values());
|
|
176
|
+
writeFileSync(filename, JSON.stringify(cookies, null, 2));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Export cookies as Puppeteer format
|
|
181
|
+
*
|
|
182
|
+
* @returns {Array} Cookies in Puppeteer format
|
|
183
|
+
*/
|
|
184
|
+
toPuppeteerFormat() {
|
|
185
|
+
return Array.from(this.cookies.values()).map(cookie => ({
|
|
186
|
+
name: cookie.name,
|
|
187
|
+
value: cookie.value,
|
|
188
|
+
domain: cookie.domain,
|
|
189
|
+
path: cookie.path,
|
|
190
|
+
expires: cookie.expires ? Math.floor(cookie.expires.getTime() / 1000) : -1,
|
|
191
|
+
maxAge: cookie.maxAge,
|
|
192
|
+
secure: cookie.secure,
|
|
193
|
+
httpOnly: cookie.httpOnly,
|
|
194
|
+
sameSite: cookie.sameSite
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get cookie count
|
|
200
|
+
*
|
|
201
|
+
* @returns {number} Number of cookies
|
|
202
|
+
*/
|
|
203
|
+
get size() {
|
|
204
|
+
return this.cookies.size;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create a CookieJar from a cookie string
|
|
209
|
+
*
|
|
210
|
+
* @param {string} cookieString - Cookie string
|
|
211
|
+
* @param {string} url - URL context
|
|
212
|
+
* @returns {CookieJar} New CookieJar instance
|
|
213
|
+
*/
|
|
214
|
+
static fromCookieString(cookieString, url) {
|
|
215
|
+
const jar = new CookieJar();
|
|
216
|
+
const parsedUrl = parse(url);
|
|
217
|
+
const domain = parsedUrl.hostname;
|
|
218
|
+
|
|
219
|
+
const parts = cookieString.split(';');
|
|
220
|
+
for (const part of parts) {
|
|
221
|
+
const [name, ...valueParts] = part.trim().split('=');
|
|
222
|
+
if (name && valueParts.length > 0) {
|
|
223
|
+
jar.set(name.trim(), valueParts.join('=').trim(), {
|
|
224
|
+
domain: domain.startsWith('.') ? domain : `.${domain}`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return jar;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Private methods
|
|
233
|
+
|
|
234
|
+
_makeKey(cookie) {
|
|
235
|
+
return `${cookie.domain}:${cookie.path}:${cookie.name}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_findCookie(name, url) {
|
|
239
|
+
const parsedUrl = parse(url);
|
|
240
|
+
const domain = parsedUrl.hostname;
|
|
241
|
+
const path = parsedUrl.pathname;
|
|
242
|
+
const isSecure = parsedUrl.protocol === 'https:';
|
|
243
|
+
|
|
244
|
+
for (const cookie of this.cookies.values()) {
|
|
245
|
+
if (cookie.name === name && this._matchesCookie(cookie, domain, path, isSecure)) {
|
|
246
|
+
return cookie;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_matchesCookie(cookie, domain, path, isSecure) {
|
|
254
|
+
// Check domain
|
|
255
|
+
if (cookie.domain) {
|
|
256
|
+
if (cookie.domain.startsWith('.')) {
|
|
257
|
+
if (!domain.endsWith(cookie.domain)) return false;
|
|
258
|
+
} else {
|
|
259
|
+
if (domain !== cookie.domain) return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check path
|
|
264
|
+
if (cookie.path) {
|
|
265
|
+
if (!path.startsWith(cookie.path)) return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check secure
|
|
269
|
+
if (cookie.secure && !isSecure) return false;
|
|
270
|
+
|
|
271
|
+
// Check expiration
|
|
272
|
+
if (cookie.expires && cookie.expires < new Date()) return false;
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|