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.
@@ -0,0 +1,416 @@
1
+ /**
2
+ * WebSocket class with TLS fingerprint spoofing
3
+ *
4
+ * Compatible with the 'ws' package API but adds:
5
+ * - TLS fingerprint spoofing via Go bridge
6
+ * - Cookie injection for Cloudflare bypass
7
+ * - Proxy support (SOCKS5, HTTP)
8
+ */
9
+
10
+ import { EventEmitter } from 'events';
11
+ import { spawn } from 'child_process';
12
+ import { join, dirname } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import { existsSync } from 'fs';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+
18
+ // Default bridge binary location
19
+ const DEFAULT_BRIDGE_DIR = join(__dirname, '..', 'bin');
20
+
21
+ export class WebSocket extends EventEmitter {
22
+ // Ready state constants (RFC 6455)
23
+ static CONNECTING = 0;
24
+ static OPEN = 1;
25
+ static CLOSING = 2;
26
+ static CLOSED = 3;
27
+
28
+ /**
29
+ * Create a new WebSocket connection
30
+ *
31
+ * @param {string} url - WebSocket URL
32
+ * @param {Object} options - Connection options
33
+ * @param {string} [options.fingerprint='chrome120'] - TLS fingerprint profile
34
+ * @param {string|Array|Object} [options.cookies] - Cookies to send
35
+ * @param {string} [options.proxy] - Proxy URL (socks5:// or http://)
36
+ * @param {boolean} [options.perMessageDeflate=true] - Enable compression
37
+ * @param {Object} [options.headers] - Additional HTTP headers
38
+ */
39
+ constructor(url, options = {}) {
40
+ super();
41
+
42
+ this.url = url;
43
+ this.options = options;
44
+
45
+ // Connection state
46
+ this.readyState = WebSocket.CONNECTING;
47
+ this.binaryType = 'nodebuffer';
48
+ this.protocol = '';
49
+ this.extensions = '';
50
+ this.bufferedAmount = 0;
51
+
52
+ // Bridge process
53
+ this._bridge = null;
54
+ this._buffer = '';
55
+ this._closed = false;
56
+
57
+ // Connect asynchronously
58
+ setImmediate(() => this._connect());
59
+ }
60
+
61
+ /**
62
+ * Establish connection via the Go bridge
63
+ */
64
+ async _connect() {
65
+ const bridgePath = this._getBridgePath();
66
+
67
+ // Check if bridge exists, if not try to use prebuild
68
+ let binaryPath = bridgePath;
69
+ if (!existsSync(bridgePath)) {
70
+ binaryPath = this._getPrebuildPath();
71
+ }
72
+
73
+ // If no binary found, emit error
74
+ if (!existsSync(binaryPath)) {
75
+ this._handleError(new Error(
76
+ `Stealth bridge not found. Please run: npm run postinstall\n` +
77
+ `Tried paths:\n - ${bridgePath}\n - ${binaryPath}`
78
+ ));
79
+ return;
80
+ }
81
+
82
+ try {
83
+ this._bridge = spawn(binaryPath, [], {
84
+ stdio: ['pipe', 'pipe', 'inherit'],
85
+ windowsHide: true
86
+ });
87
+
88
+ // Handle stdin errors (e.g., EPIPE when process exits)
89
+ this._bridge.stdin.on('error', (err) => {
90
+ if (err.code !== 'EPIPE' && !this._closed) {
91
+ this._handleError(err);
92
+ }
93
+ });
94
+
95
+ // Collect and parse messages from bridge
96
+ this._bridge.stdout.on('data', (chunk) => {
97
+ this._buffer += chunk.toString();
98
+ this._processBuffer();
99
+ });
100
+
101
+ this._bridge.on('exit', (code) => {
102
+ if (!this._closed) {
103
+ this.readyState = WebSocket.CLOSED;
104
+ this.emit('close', 1006, '');
105
+ }
106
+ });
107
+
108
+ this._bridge.on('error', (err) => {
109
+ this._handleError(err);
110
+ });
111
+
112
+ // Send configuration to bridge
113
+ this._sendConfig();
114
+
115
+ } catch (err) {
116
+ this._handleError(err);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Send configuration to the bridge
122
+ */
123
+ _sendConfig() {
124
+ const config = {
125
+ type: 'connect',
126
+ url: this.url,
127
+ fingerprint: this.options.fingerprint || 'chrome120',
128
+ cookies: this._formatCookies(this.options.cookies),
129
+ proxy: this.options.proxy,
130
+ headers: this.options.headers || {},
131
+ perMessageDeflate: this.options.perMessageDeflate !== false
132
+ };
133
+
134
+ this._bridge.stdin.write(JSON.stringify(config) + '\n');
135
+ }
136
+
137
+ /**
138
+ * Process received data from bridge
139
+ */
140
+ _processBuffer() {
141
+ const lines = this._buffer.split('\n');
142
+ this._buffer = lines.pop();
143
+
144
+ for (const line of lines) {
145
+ if (!line.trim()) continue;
146
+ try {
147
+ const msg = JSON.parse(line);
148
+ this._handleMessage(msg);
149
+ } catch (e) {
150
+ // Ignore malformed messages
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Handle messages from the bridge
157
+ */
158
+ _handleMessage(msg) {
159
+ switch (msg.type) {
160
+ case 'open':
161
+ this.readyState = WebSocket.OPEN;
162
+ this.protocol = msg.protocol || '';
163
+ this.extensions = msg.extensions || '';
164
+ this.emit('open');
165
+ break;
166
+
167
+ case 'message':
168
+ this.emit('message', Buffer.from(msg.data), false);
169
+ break;
170
+
171
+ case 'binary':
172
+ this.emit('message', Buffer.from(msg.data), true);
173
+ break;
174
+
175
+ case 'ping':
176
+ // Auto-pong for ping messages
177
+ if (this.readyState === WebSocket.OPEN) {
178
+ this.pong();
179
+ }
180
+ this.emit('ping', msg.data ? Buffer.from(msg.data) : undefined);
181
+ break;
182
+
183
+ case 'close':
184
+ this.readyState = WebSocket.CLOSED;
185
+ this.emit('close', msg.code || 1000, msg.reason || '');
186
+ break;
187
+
188
+ case 'auth_required':
189
+ this.emit('auth_required');
190
+ break;
191
+
192
+ case 'error':
193
+ this._handleError(new Error(msg.message));
194
+ break;
195
+
196
+ case 'debug':
197
+ // Debug messages (can be logged if DEBUG enabled)
198
+ if (this.options.debug) {
199
+ console.log('[stealth-ws:debug]', msg.message);
200
+ }
201
+ break;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Handle errors
207
+ */
208
+ _handleError(err) {
209
+ if (this.readyState === WebSocket.CLOSED) return;
210
+
211
+ this.readyState = WebSocket.CLOSED;
212
+ this.emit('error', err);
213
+ }
214
+
215
+ /**
216
+ * Send data through the WebSocket
217
+ *
218
+ * @param {string|Buffer} data - Data to send
219
+ * @param {Object} options - Send options
220
+ * @param {Function} callback - Callback when sent
221
+ */
222
+ send(data, options = {}, callback) {
223
+ if (this.readyState !== WebSocket.OPEN) {
224
+ const err = new Error('WebSocket is not open');
225
+ if (callback) callback(err);
226
+ else this.emit('error', err);
227
+ return;
228
+ }
229
+
230
+ const msg = {
231
+ type: 'send',
232
+ data: Buffer.isBuffer(data) ? data.toString('base64') : String(data),
233
+ binary: options.binary ?? Buffer.isBuffer(data)
234
+ };
235
+
236
+ try {
237
+ this._bridge.stdin.write(JSON.stringify(msg) + '\n');
238
+ if (callback) callback();
239
+ } catch (err) {
240
+ if (callback) callback(err);
241
+ else this.emit('error', err);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Send a ping frame
247
+ */
248
+ ping(data, mask, callback) {
249
+ if (this.readyState !== WebSocket.OPEN) return;
250
+
251
+ const msg = {
252
+ type: 'ping',
253
+ data: data ? (Buffer.isBuffer(data) ? data.toString('base64') : String(data)) : undefined
254
+ };
255
+
256
+ try {
257
+ this._bridge.stdin.write(JSON.stringify(msg) + '\n');
258
+ if (callback) callback();
259
+ } catch (err) {
260
+ if (callback) callback(err);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Send a pong frame
266
+ */
267
+ pong(data, mask, callback) {
268
+ if (this.readyState !== WebSocket.OPEN) return;
269
+
270
+ const msg = {
271
+ type: 'pong',
272
+ data: data ? (Buffer.isBuffer(data) ? data.toString('base64') : String(data)) : undefined
273
+ };
274
+
275
+ try {
276
+ this._bridge.stdin.write(JSON.stringify(msg) + '\n');
277
+ if (callback) callback();
278
+ } catch (err) {
279
+ if (callback) callback(err);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Close the WebSocket connection
285
+ *
286
+ * @param {number} code - Close code
287
+ * @param {string} reason - Close reason
288
+ */
289
+ close(code = 1000, reason = '') {
290
+ if (this.readyState === WebSocket.CLOSED) return;
291
+ if (this.readyState === WebSocket.CLOSING) return;
292
+
293
+ this.readyState = WebSocket.CLOSING;
294
+ this._closed = true;
295
+
296
+ const msg = {
297
+ type: 'close',
298
+ code,
299
+ reason: String(reason)
300
+ };
301
+
302
+ try {
303
+ this._bridge.stdin.write(JSON.stringify(msg) + '\n');
304
+ } catch (err) {
305
+ // Ignore errors during close
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Forcibly terminate the connection
311
+ */
312
+ terminate() {
313
+ this._closed = true;
314
+ this.readyState = WebSocket.CLOSED;
315
+
316
+ if (this._bridge) {
317
+ try {
318
+ this._bridge.kill();
319
+ } catch (err) {
320
+ // Ignore kill errors
321
+ }
322
+ this._bridge = null;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Pause the connection
328
+ */
329
+ pause() {
330
+ // Not implemented in bridge mode
331
+ }
332
+
333
+ /**
334
+ * Resume the connection
335
+ */
336
+ resume() {
337
+ // Not implemented in bridge mode
338
+ }
339
+
340
+ /**
341
+ * Format cookies for bridge
342
+ */
343
+ _formatCookies(cookies) {
344
+ if (!cookies) return '';
345
+ if (typeof cookies === 'string') return cookies;
346
+ if (Array.isArray(cookies)) {
347
+ return cookies.map(c => `${c.name}=${c.value}`).join('; ');
348
+ }
349
+ if (typeof cookies === 'object' && cookies.getCookieString) {
350
+ return cookies.getCookieString(this.url);
351
+ }
352
+ return String(cookies);
353
+ }
354
+
355
+ /**
356
+ * Get bridge binary path for current platform
357
+ */
358
+ _getBridgePath() {
359
+ return join(DEFAULT_BRIDGE_DIR, this._getBridgeName());
360
+ }
361
+
362
+ /**
363
+ * Get prebuild path for current platform
364
+ */
365
+ _getPrebuildPath() {
366
+ const platform = process.platform;
367
+ const arch = process.arch;
368
+
369
+ const platformMap = {
370
+ 'win32': 'win32',
371
+ 'linux': 'linux',
372
+ 'darwin': 'darwin'
373
+ };
374
+
375
+ const archMap = {
376
+ 'x64': 'x64',
377
+ 'arm64': 'arm64'
378
+ };
379
+
380
+ const p = platformMap[platform] || platform;
381
+ const a = archMap[arch] || arch;
382
+
383
+ const ext = platform === 'win32' ? '.exe' : '';
384
+ return join(__dirname, '..', 'prebuilds', `${p}-${a}`, `stealth-bridge${ext}`);
385
+ }
386
+
387
+ /**
388
+ * Get bridge binary name for current platform
389
+ */
390
+ _getBridgeName() {
391
+ if (process.platform === 'win32') {
392
+ return 'stealth-bridge.exe';
393
+ }
394
+ return 'stealth-bridge';
395
+ }
396
+ }
397
+
398
+ // Add EventTarget compatibility
399
+ WebSocket.prototype.addEventListener = function(type, listener, options) {
400
+ if (options && options.once) {
401
+ this.on(type, function wrapper(...args) {
402
+ this.removeListener(type, wrapper);
403
+ listener.apply(this, args);
404
+ });
405
+ } else {
406
+ this.on(type, listener);
407
+ }
408
+ };
409
+
410
+ WebSocket.prototype.removeEventListener = function(type, listener) {
411
+ this.removeListener(type, listener);
412
+ };
413
+
414
+ WebSocket.prototype.dispatchEvent = function(event) {
415
+ this.emit(event.type, event);
416
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "stealth-ws",
3
+ "version": "1.0.0",
4
+ "description": "WebSocket client with TLS fingerprint spoofing",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./lib/index.js"
10
+ },
11
+ "scripts": {
12
+ "test": "node --test test/*.test.js",
13
+ "postinstall": "node scripts/download-binary.js",
14
+ "build:go": "node scripts/build-go.js",
15
+ "prebuild": "node scripts/build-all.js"
16
+ },
17
+ "keywords": [
18
+ "websocket",
19
+ "stealth",
20
+ "fingerprint",
21
+ "tls",
22
+ "utls",
23
+ "proxy",
24
+ "browser-emulation"
25
+ ],
26
+ "author": "safra36",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/yourusername/stealth-ws.git"
31
+ },
32
+ "dependencies": {},
33
+ "devDependencies": {},
34
+ "optionalDependencies": {
35
+ "@stealth-ws/linux-x64": "1.0.0",
36
+ "@stealth-ws/darwin-x64": "1.0.0",
37
+ "@stealth-ws/win32-x64": "1.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "os": [
43
+ "linux",
44
+ "darwin",
45
+ "win32"
46
+ ],
47
+ "cpu": [
48
+ "x64"
49
+ ],
50
+ "files": [
51
+ "lib/",
52
+ "bin/",
53
+ "prebuilds/",
54
+ "scripts/",
55
+ "README.md",
56
+ "LICENSE"
57
+ ]
58
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Build script for all supported platform binaries
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { mkdirSync, existsSync } from 'fs';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const ROOT_DIR = join(__dirname, '..');
14
+ const PREBUILTS_DIR = join(ROOT_DIR, 'prebuilds');
15
+
16
+ // Supported x64 platforms only (ARM builds not supported - build from source with: npm run build:go)
17
+ const PLATFORMS = [
18
+ { name: 'win32-x64', os: 'windows', arch: 'amd64', ext: '.exe' },
19
+ { name: 'linux-x64', os: 'linux', arch: 'amd64', ext: '' },
20
+ { name: 'darwin-x64', os: 'darwin', arch: 'amd64', ext: '' }
21
+ ];
22
+
23
+ console.log('Note: ARM builds not supported (build from source with: npm run build:go)\n');
24
+
25
+ function runCommand(cmd, args, options = {}) {
26
+ return new Promise((resolve, reject) => {
27
+ const proc = spawn(cmd, args, {
28
+ ...options,
29
+ stdio: 'inherit'
30
+ });
31
+
32
+ proc.on('close', (code) => {
33
+ if (code === 0) {
34
+ resolve();
35
+ } else {
36
+ reject(new Error(`Command failed with exit code ${code}`));
37
+ }
38
+ });
39
+
40
+ proc.on('error', reject);
41
+ });
42
+ }
43
+
44
+ async function buildForPlatform(platform) {
45
+ const outputName = `stealth-bridge${platform.ext}`;
46
+ const outputDir = join(PREBUILTS_DIR, platform.name);
47
+ const outputPath = join(outputDir, outputName);
48
+
49
+ console.log(`Building for ${platform.name}...`);
50
+
51
+ if (!existsSync(outputDir)) {
52
+ mkdirSync(outputDir, { recursive: true });
53
+ }
54
+
55
+ const goCmd = process.platform === 'win32' ? 'go.exe' : 'go';
56
+ const srcDir = join(ROOT_DIR, 'src', 'bridge');
57
+
58
+ try {
59
+ await runCommand(goCmd, [
60
+ 'build',
61
+ '-o', outputPath,
62
+ '-ldflags', '-s -w',
63
+ srcDir
64
+ ], {
65
+ env: {
66
+ ...process.env,
67
+ GOOS: platform.os,
68
+ GOARCH: platform.arch,
69
+ CGO_ENABLED: '0'
70
+ },
71
+ cwd: srcDir
72
+ });
73
+
74
+ console.log(`✓ Built: ${outputPath}`);
75
+ return true;
76
+
77
+ } catch (err) {
78
+ console.error(`✗ Build failed for ${platform.name}:`, err.message);
79
+ return false;
80
+ }
81
+ }
82
+
83
+ async function buildAll() {
84
+ console.log('Building stealth-bridge binaries for all platforms...\n');
85
+
86
+ let successCount = 0;
87
+ for (const platform of PLATFORMS) {
88
+ const success = await buildForPlatform(platform);
89
+ if (success) successCount++;
90
+ }
91
+
92
+ console.log(`\nBuild complete: ${successCount}/${PLATFORMS.length} platforms succeeded`);
93
+
94
+ if (successCount === PLATFORMS.length) {
95
+ console.log('\nAll binaries built successfully!');
96
+ } else {
97
+ console.log('\nSome builds failed. Check errors above.');
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ buildAll().catch(console.error);
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Build script for the Go bridge
3
+ *
4
+ * Builds the stealth-bridge binary for the current platform.
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+ import { join, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { existsSync, mkdirSync } from 'fs';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const ROOT_DIR = join(__dirname, '..');
14
+ const SRC_DIR = join(ROOT_DIR, 'src', 'bridge');
15
+ const BIN_DIR = join(ROOT_DIR, 'bin');
16
+
17
+ // Platform configurations
18
+ const PLATFORMS = {
19
+ 'win32': { os: 'windows', arch: 'amd64', ext: '.exe' },
20
+ 'win32-arm64': { os: 'windows', arch: 'arm64', ext: '.exe' },
21
+ 'linux': { os: 'linux', arch: 'amd64', ext: '' },
22
+ 'linux-arm64': { os: 'linux', arch: 'arm64', ext: '' },
23
+ 'darwin': { os: 'darwin', arch: 'amd64', ext: '' },
24
+ 'darwin-arm64': { os: 'darwin', arch: 'arm64', ext: '' }
25
+ };
26
+
27
+ function getPlatform() {
28
+ const platform = process.platform;
29
+ const arch = process.arch;
30
+
31
+ if (platform === 'win32') {
32
+ return arch === 'arm64' ? 'win32-arm64' : 'win32';
33
+ }
34
+ if (platform === 'linux') {
35
+ return arch === 'arm64' ? 'linux-arm64' : 'linux';
36
+ }
37
+ if (platform === 'darwin') {
38
+ return arch === 'arm64' ? 'darwin-arm64' : 'darwin';
39
+ }
40
+
41
+ return null;
42
+ }
43
+
44
+ function runCommand(cmd, args, options = {}) {
45
+ return new Promise((resolve, reject) => {
46
+ const proc = spawn(cmd, args, {
47
+ ...options,
48
+ stdio: 'inherit'
49
+ });
50
+
51
+ proc.on('close', (code) => {
52
+ if (code === 0) {
53
+ resolve();
54
+ } else {
55
+ reject(new Error(`Command failed with exit code ${code}`));
56
+ }
57
+ });
58
+
59
+ proc.on('error', reject);
60
+ });
61
+ }
62
+
63
+ async function buildForPlatform(platform) {
64
+ const config = PLATFORMS[platform];
65
+ if (!config) {
66
+ console.log(`Skipping unsupported platform: ${platform}`);
67
+ return false;
68
+ }
69
+
70
+ const outputName = `stealth-bridge${config.ext}`;
71
+ const outputPath = join(BIN_DIR, outputName);
72
+
73
+ console.log(`Building for ${platform}...`);
74
+
75
+ // Ensure bin directory exists
76
+ if (!existsSync(BIN_DIR)) {
77
+ mkdirSync(BIN_DIR, { recursive: true });
78
+ }
79
+
80
+ // Build command - detect Go command
81
+ const goCmd = process.platform === 'win32' ? 'go.exe' : 'go';
82
+
83
+ try {
84
+ // Step 1: Download dependencies
85
+ console.log('Downloading Go dependencies...');
86
+ await runCommand(goCmd, ['mod', 'download'], { cwd: SRC_DIR });
87
+
88
+ // Step 2: Tidy modules
89
+ console.log('Tidying Go modules...');
90
+ await runCommand(goCmd, ['mod', 'tidy'], { cwd: SRC_DIR });
91
+
92
+ // Step 3: Build binary
93
+ console.log('Building binary...');
94
+ await runCommand(goCmd, [
95
+ 'build',
96
+ '-o', outputPath,
97
+ '-ldflags', '-s -w',
98
+ SRC_DIR
99
+ ], {
100
+ env: {
101
+ ...process.env,
102
+ GOOS: config.os,
103
+ GOARCH: config.arch,
104
+ CGO_ENABLED: '0'
105
+ },
106
+ cwd: SRC_DIR
107
+ });
108
+
109
+ console.log(`✓ Built: ${outputPath}`);
110
+ return true;
111
+
112
+ } catch (err) {
113
+ console.error(`✗ Build failed for ${platform}:`, err.message);
114
+ return false;
115
+ }
116
+ }
117
+
118
+ async function buildAll() {
119
+ console.log('Building stealth-bridge binaries...\n');
120
+
121
+ // Build for current platform
122
+ const platform = getPlatform();
123
+ if (platform) {
124
+ await buildForPlatform(platform);
125
+ } else {
126
+ console.error('Unsupported platform');
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ // Run if called directly
132
+ buildAll().catch(console.error);
133
+
134
+ export { buildForPlatform, buildAll };