shell-cluster
Advanced tools
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| const http = require('http'); | ||
| const { WebSocketServer, WebSocket } = require('ws'); | ||
| const pty = require('node-pty'); | ||
| const os = require('os'); | ||
| const PORT = 19877; | ||
| // Test 1: Direct spawn | ||
| console.log('=== Test 1: Direct spawn ==='); | ||
| try { | ||
| const p = pty.spawn('/bin/zsh', [], { | ||
| name: 'xterm-256color', cols: 80, rows: 24, | ||
| cwd: os.homedir(), | ||
| env: Object.assign({}, process.env, { TERM: 'xterm-256color' }), | ||
| }); | ||
| console.log('OK: pid=' + p.pid); | ||
| p.kill(); | ||
| } catch (e) { | ||
| console.log('FAIL:', e.message); | ||
| } | ||
| // Test 2: Spawn inside WS handler | ||
| console.log('\n=== Test 2: Spawn inside WebSocket handler ==='); | ||
| const server = http.createServer(); | ||
| const wss = new WebSocketServer({ noServer: true }); | ||
| server.on('upgrade', (req, socket, head) => { | ||
| wss.handleUpgrade(req, socket, head, (ws) => { | ||
| console.log(' WS client connected'); | ||
| try { | ||
| const p = pty.spawn('/bin/zsh', [], { | ||
| name: 'xterm-256color', cols: 80, rows: 24, | ||
| cwd: os.homedir(), | ||
| env: Object.assign({}, process.env, { TERM: 'xterm-256color' }), | ||
| }); | ||
| console.log(' OK: pid=' + p.pid); | ||
| ws.send('OK:' + p.pid); | ||
| p.kill(); | ||
| } catch (e) { | ||
| console.log(' FAIL:', e.message); | ||
| ws.send('FAIL:' + e.message); | ||
| } | ||
| ws.close(); | ||
| }); | ||
| }); | ||
| server.listen(PORT, '127.0.0.1', () => { | ||
| console.log(' Server on port ' + PORT); | ||
| const ws = new WebSocket(`ws://127.0.0.1:${PORT}/raw?session=test&cols=80&rows=24`); | ||
| ws.on('message', (data) => { | ||
| console.log(' Client received: ' + data.toString()); | ||
| }); | ||
| ws.on('close', () => { | ||
| server.close(() => { | ||
| console.log('\nDone.'); | ||
| process.exit(0); | ||
| }); | ||
| }); | ||
| ws.on('error', (e) => { | ||
| console.log(' Client error: ' + e.message); | ||
| server.close(() => process.exit(1)); | ||
| }); | ||
| }); |
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| const { WebSocket } = require('ws'); | ||
| const sessionId = 'diag-' + Date.now(); | ||
| const url = `ws://127.0.0.1:19876/raw?session=${sessionId}&cols=80&rows=24`; | ||
| console.log('Connecting to:', url); | ||
| const ws = new WebSocket(url); | ||
| ws.on('open', () => console.log('WS opened')); | ||
| ws.on('message', (data, isBinary) => { | ||
| if (isBinary) { | ||
| console.log('Binary data:', data.length, 'bytes'); | ||
| } else { | ||
| const text = data.toString(); | ||
| try { | ||
| const msg = JSON.parse(text); | ||
| console.log('JSON:', JSON.stringify(msg)); | ||
| } catch { | ||
| console.log('Text:', text.slice(0, 200)); | ||
| } | ||
| } | ||
| }); | ||
| ws.on('close', (code, reason) => { | ||
| console.log(`Closed: code=${code} reason=${reason.toString()}`); | ||
| process.exit(0); | ||
| }); | ||
| ws.on('error', (e) => { | ||
| console.log('Error:', e.message); | ||
| process.exit(1); | ||
| }); | ||
| setTimeout(() => { | ||
| console.log('Timeout - closing'); | ||
| ws.close(); | ||
| }, 3000); |
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| const { WebSocket } = require('ws'); | ||
| // Test both localhost and 127.0.0.1 | ||
| for (const host of ['localhost', '127.0.0.1']) { | ||
| const sessionId = 'diag-' + host + '-' + Date.now(); | ||
| const url = `ws://${host}:19876/raw?session=${sessionId}&cols=80&rows=24`; | ||
| console.log(`\n--- Testing ${host} ---`); | ||
| console.log('URL:', url); | ||
| const ws = new WebSocket(url); | ||
| ws.on('open', () => console.log(` [${host}] opened`)); | ||
| ws.on('message', (data, isBinary) => { | ||
| if (!isBinary) { | ||
| try { | ||
| const msg = JSON.parse(data.toString()); | ||
| console.log(` [${host}] JSON:`, msg.type); | ||
| } catch { | ||
| console.log(` [${host}] text:`, data.toString().slice(0, 80)); | ||
| } | ||
| } else { | ||
| console.log(` [${host}] binary: ${data.length} bytes`); | ||
| } | ||
| }); | ||
| ws.on('close', (code, reason) => { | ||
| console.log(` [${host}] closed: code=${code} reason=${reason.toString()}`); | ||
| }); | ||
| ws.on('error', (e) => { | ||
| console.log(` [${host}] ERROR: ${e.message}`); | ||
| }); | ||
| setTimeout(() => ws.close(), 2000); | ||
| } | ||
| setTimeout(() => process.exit(0), 3000); |
+1
-1
| { | ||
| "name": "shell-cluster", | ||
| "version": "1.0.8", | ||
| "version": "1.0.9", | ||
| "description": "Decentralized remote shell access via tunnels — Node.js server with node-pty and xterm-headless", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
+7
-3
@@ -455,5 +455,8 @@ // --- State --- | ||
| ws.onclose = () => { | ||
| ws.onclose = (ev) => { | ||
| console.warn(`[WS] closed session=${sessionId} code=${ev.code} reason="${ev.reason}"`); | ||
| if (!attached) { | ||
| term.writeln('\r\n\x1b[2m[Disconnected]\x1b[0m'); | ||
| term.writeln(`\r\n\x1b[2m[Disconnected: code=${ev.code}${ev.reason ? ' ' + ev.reason : ''}]\x1b[0m`); | ||
| } else { | ||
| term.writeln(`\r\n\x1b[2m[Disconnected: code=${ev.code}${ev.reason ? ' ' + ev.reason : ''}]\x1b[0m`); | ||
| } | ||
@@ -464,3 +467,4 @@ sessionState._disconnected = true; | ||
| ws.onerror = () => { | ||
| ws.onerror = (ev) => { | ||
| console.error(`[WS] error session=${sessionId}`, ev); | ||
| term.writeln('\r\n\x1b[31m[Connection error]\x1b[0m'); | ||
@@ -467,0 +471,0 @@ }; |
@@ -209,3 +209,4 @@ /** | ||
| peerWs.on('close', () => { | ||
| peerWs.on('close', (code, reason) => { | ||
| console.log(`[DashboardServer] Peer WS closed code=${code} reason="${reason || ''}"`); | ||
| browserWs.close(); | ||
@@ -225,3 +226,4 @@ }); | ||
| browserWs.on('close', () => { | ||
| browserWs.on('close', (code, reason) => { | ||
| console.log(`[DashboardServer] Browser WS closed code=${code} reason="${reason || ''}"`); | ||
| if (peerWs && peerWs.readyState === WebSocket.OPEN) { | ||
@@ -228,0 +230,0 @@ peerWs.close(); |
+19
-7
@@ -71,9 +71,21 @@ /** | ||
| const ptyProcess = pty.spawn(shellCmd, [], { | ||
| name: 'xterm-256color', | ||
| cols, | ||
| rows, | ||
| cwd: os.homedir(), | ||
| env, | ||
| }); | ||
| let ptyProcess; | ||
| try { | ||
| ptyProcess = pty.spawn(shellCmd, [], { | ||
| name: 'xterm-256color', | ||
| cols, | ||
| rows, | ||
| cwd: os.homedir(), | ||
| env, | ||
| }); | ||
| } catch (e) { | ||
| const fs = require('fs'); | ||
| const shellExists = fs.existsSync(shellCmd); | ||
| const homeExists = fs.existsSync(os.homedir()); | ||
| console.log(`[ShellManager] ERROR: Failed to spawn shell '${shellCmd}'`); | ||
| console.log(`[ShellManager] shell exists: ${shellExists}, cwd exists: ${homeExists}, cols=${cols}, rows=${rows}`); | ||
| console.log(`[ShellManager] node-pty error: ${e.message}`); | ||
| if (e.stack) console.log(e.stack); | ||
| throw new Error(`Failed to spawn '${shellCmd}': ${e.message} (shell exists=${shellExists})`); | ||
| } | ||
@@ -80,0 +92,0 @@ // Create headless terminal for state tracking |
+65
-27
@@ -122,27 +122,57 @@ /** | ||
| // --- Batched output: accumulate PTY data, flush every 16ms (~60fps) --- | ||
| let outputBuf = []; | ||
| let flushTimer = null; | ||
| const flushOutput = () => { | ||
| flushTimer = null; | ||
| if (outputBuf.length === 0) return; | ||
| if (ws.readyState !== ws.OPEN) { | ||
| outputBuf = []; | ||
| return; | ||
| } | ||
| const combined = Buffer.concat(outputBuf); | ||
| outputBuf = []; | ||
| let str = combined.toString('utf-8'); | ||
| str = stripTerminalQueries(str); | ||
| if (!str) return; | ||
| try { | ||
| ws.send(Buffer.from(str, 'utf-8'), (err) => { | ||
| if (err) { | ||
| console.warn(`[ShellServer] ws.send error session=${sessionId}: ${err.message}`); | ||
| return; | ||
| } | ||
| // Backpressure: pause PTY if WS buffer > 1MB | ||
| if (ws.bufferedAmount > 1024 * 1024) { | ||
| console.log(`[ShellServer] Backpressure ON session=${sessionId} buffered=${ws.bufferedAmount}`); | ||
| this._shellManager.pausePty(sessionId); | ||
| const check = () => { | ||
| if (ws.readyState !== ws.OPEN) { | ||
| // WS gone — resume PTY so it doesn't stay paused forever | ||
| this._shellManager.resumePty(sessionId); | ||
| return; | ||
| } | ||
| if (ws.bufferedAmount < 256 * 1024) { | ||
| console.log(`[ShellServer] Backpressure OFF session=${sessionId}`); | ||
| this._shellManager.resumePty(sessionId); | ||
| } else { | ||
| setTimeout(check, 50); | ||
| } | ||
| }; | ||
| setTimeout(check, 50); | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| console.warn(`[ShellServer] ws.send threw session=${sessionId}: ${e.message}`); | ||
| } | ||
| }; | ||
| const onOutput = (sid, data) => { | ||
| if (ws.readyState !== ws.OPEN) return; | ||
| let str = data.toString('utf-8'); | ||
| str = stripTerminalQueries(str); | ||
| if (str) { | ||
| try { | ||
| ws.send(Buffer.from(str, 'utf-8'), (err) => { | ||
| if (err) return; | ||
| // Backpressure: pause PTY if WS buffer > 1MB | ||
| if (ws.bufferedAmount > 1024 * 1024) { | ||
| this._shellManager.pausePty(sessionId); | ||
| const check = () => { | ||
| if (ws.readyState !== ws.OPEN) return; | ||
| if (ws.bufferedAmount < 256 * 1024) { | ||
| this._shellManager.resumePty(sessionId); | ||
| } else { | ||
| setTimeout(check, 50); | ||
| } | ||
| }; | ||
| setTimeout(check, 50); | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| // connection closed | ||
| } | ||
| outputBuf.push(data); | ||
| if (!flushTimer) { | ||
| flushTimer = setTimeout(flushOutput, 67); | ||
| } | ||
@@ -196,3 +226,4 @@ }; | ||
| } catch (e) { | ||
| console.error(`[ShellServer] Session setup failed:`, e.message); | ||
| console.log(`[ShellServer] ERROR: Session setup failed for session=${sessionId}: ${e.message}`); | ||
| if (e.stack) console.log(e.stack); | ||
| try { | ||
@@ -240,9 +271,16 @@ ws.send(JSON.stringify({ type: 'error', error: e.message })); | ||
| ws.on('close', () => { | ||
| console.log(`[ShellServer] Raw client disconnected: session=${sessionId}`); | ||
| ws.on('close', (code, reason) => { | ||
| const reasonStr = reason ? reason.toString() : ''; | ||
| console.log(`[ShellServer] WS closed session=${sessionId} code=${code} reason="${reasonStr}"`); | ||
| // Clean up flush timer | ||
| if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } | ||
| outputBuf = []; | ||
| this._shellManager.detach(sessionId, onOutput, onExit); | ||
| // Ensure PTY is resumed in case backpressure left it paused | ||
| this._shellManager.resumePty(sessionId); | ||
| }); | ||
| ws.on('error', (err) => { | ||
| console.warn(`[ShellServer] WebSocket error for session=${sessionId}:`, err.message); | ||
| console.error(`[ShellServer] WS error session=${sessionId}: ${err.message}`); | ||
| if (err.code) console.error(`[ShellServer] code=${err.code}`); | ||
| }); | ||
@@ -249,0 +287,0 @@ } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
116514
5.89%22
15.79%3277
5.68%19
18.75%17
6.25%