shell-cluster
Advanced tools
+1
-1
| { | ||
| "name": "shell-cluster", | ||
| "version": "1.0.13", | ||
| "version": "1.0.14", | ||
| "description": "Decentralized remote shell access via tunnels — Node.js server with node-pty and xterm-headless", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
+10
-2
@@ -166,3 +166,3 @@ # Shell Cluster | ||
| - **HTTP** `/sessions` endpoint: list active sessions | ||
| - **Dashboard API** (port 9000): `/api/peers`, `/api/refresh-peers`, WebSocket proxy | ||
| - **Dashboard API** (port 9000): `/api/peers`, `/api/version`, `/api/refresh-peers`, WebSocket proxy | ||
@@ -201,3 +201,3 @@ ## Why Decentralized? | ||
| | Linux | `~/.config/shell-cluster/config.toml` | | ||
| | Windows | `%APPDATA%\shell-cluster\config.toml` | | ||
| | Windows | `%LOCALAPPDATA%\shell-cluster\config.toml` | | ||
@@ -230,2 +230,10 @@ ```toml | ||
| ### Running Tests | ||
| ```bash | ||
| npm test # all tests | ||
| npm run test:unit # unit tests only | ||
| npm run test:e2e # end-to-end tests only | ||
| ``` | ||
| ## Service Management | ||
@@ -232,0 +240,0 @@ |
+8
-1
@@ -67,3 +67,10 @@ /** | ||
| const raw = fs.readFileSync(CONFIG_FILE, 'utf-8'); | ||
| const data = TOML.parse(raw); | ||
| let data; | ||
| try { | ||
| data = TOML.parse(raw); | ||
| } catch (e) { | ||
| console.error(`[Config] Failed to parse ${CONFIG_FILE}: ${e.message}`); | ||
| console.error('[Config] Using default configuration'); | ||
| return defaultConfig(); | ||
| } | ||
| const config = defaultConfig(); | ||
@@ -70,0 +77,0 @@ |
@@ -41,3 +41,3 @@ /** | ||
| this._host = opts.host || '127.0.0.1'; | ||
| this._port = opts.port || 9000; | ||
| this._port = opts.port !== undefined ? opts.port : 9000; | ||
| this._getPeers = opts.getPeers || (() => []); | ||
@@ -77,6 +77,13 @@ this._refreshPeers = opts.refreshPeers || null; | ||
| const origin = req.headers.origin || ''; | ||
| if (origin && (origin.includes('://localhost') || origin.includes('://127.0.0.1'))) { | ||
| res.setHeader('Access-Control-Allow-Origin', origin); | ||
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); | ||
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); | ||
| if (origin) { | ||
| try { | ||
| const parsed = new URL(origin); | ||
| if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') { | ||
| res.setHeader('Access-Control-Allow-Origin', origin); | ||
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); | ||
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); | ||
| } | ||
| } catch (e) { | ||
| // invalid origin URL — ignore | ||
| } | ||
| } | ||
@@ -231,3 +238,3 @@ } | ||
| console.log(`[DashboardServer] Browser WS closed code=${code} reason="${reason || ''}"`); | ||
| if (peerWs && peerWs.readyState === WebSocket.OPEN) { | ||
| if (peerWs && (peerWs.readyState === WebSocket.OPEN || peerWs.readyState === WebSocket.CONNECTING)) { | ||
| peerWs.close(); | ||
@@ -238,3 +245,3 @@ } | ||
| browserWs.on('error', () => { | ||
| if (peerWs && peerWs.readyState === WebSocket.OPEN) { | ||
| if (peerWs && (peerWs.readyState === WebSocket.OPEN || peerWs.readyState === WebSocket.CONNECTING)) { | ||
| peerWs.close(); | ||
@@ -241,0 +248,0 @@ } |
@@ -283,2 +283,7 @@ /** | ||
| // Fire exit callbacks BEFORE setting _disposed so onExit won't double-fire | ||
| for (const cb of [...sess._exits]) { | ||
| try { cb(sessionId); } catch (e) { /* ignore */ } | ||
| } | ||
| sess._disposed = true; | ||
@@ -285,0 +290,0 @@ try { |
+30
-6
@@ -189,2 +189,6 @@ /** | ||
| const onExit = (sid) => { | ||
| // Flush any pending output before sending shell.closed | ||
| if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } | ||
| flushOutput(); | ||
| if (ws.readyState !== ws.OPEN) return; | ||
@@ -245,3 +249,8 @@ try { | ||
| // --- Batched PTY input: reduce write frequency to ConPTY --- | ||
| // Also dedup mouse move events — only keep the latest position per batch. | ||
| // SGR mouse move: \x1b[<35;X;YM (button=35 means motion, no button pressed) | ||
| // SGR mouse drag: \x1b[<32;X;YM \x1b[<33;X;YM \x1b[<34;X;YM | ||
| const MOUSE_MOVE_RE = /^\x1b\[<(3[2-5]);(\d+);(\d+)M$/; | ||
| let inputBuf = []; | ||
| let lastMouseMove = null; // deduplicated: only keep latest mouse move | ||
| let inputTimer = null; | ||
@@ -251,5 +260,9 @@ | ||
| inputTimer = null; | ||
| if (inputBuf.length === 0) return; | ||
| const combined = Buffer.concat(inputBuf); | ||
| const parts = inputBuf; | ||
| const tail = lastMouseMove; | ||
| inputBuf = []; | ||
| lastMouseMove = null; | ||
| if (parts.length === 0 && !tail) return; | ||
| if (tail) parts.push(tail); | ||
| const combined = Buffer.concat(parts); | ||
| this._shellManager.write(sessionId, combined); | ||
@@ -261,4 +274,15 @@ }; | ||
| if (isBinary) { | ||
| // Binary frame = PTY input — batch to reduce ConPTY pressure | ||
| inputBuf.push(data); | ||
| // Binary frame = PTY input — batch + dedup mouse moves | ||
| const str = data.toString('utf-8'); | ||
| if (MOUSE_MOVE_RE.test(str)) { | ||
| // Mouse move/drag: replace previous, don't accumulate | ||
| lastMouseMove = data; | ||
| } else { | ||
| // Non-mouse data: flush any pending mouse move first, then queue | ||
| if (lastMouseMove) { | ||
| inputBuf.push(lastMouseMove); | ||
| lastMouseMove = null; | ||
| } | ||
| inputBuf.push(data); | ||
| } | ||
| if (!inputTimer) { | ||
@@ -286,4 +310,4 @@ inputTimer = setTimeout(flushInput, 8); | ||
| } else { | ||
| // Unknown JSON — treat as PTY input | ||
| this._shellManager.write(sessionId, Buffer.from(text, 'utf-8')); | ||
| // Unknown JSON control type — ignore it (don't write to PTY) | ||
| console.warn(`[ShellServer] Unknown control type: ${ctrl.type} session=${sessionId}`); | ||
| } | ||
@@ -290,0 +314,0 @@ } catch (e) { |
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.
121285
1.61%3389
1.22%266
3.1%