const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const pty = require('node-pty'); const path = require('path'); const os = require('os'); const url = require('url'); const { execSync } = require('child_process'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ noServer: true }); const PORT = process.env.PORT || 7860; const PASSWORD = process.env.PASS || 'admin'; // Resource limits const MAX_CPU_PERCENT = 95; const MAX_MEM_PERCENT = 95; const TOTAL_MEM_MB = Math.round(os.totalmem() / 1024 / 1024); const MAX_MEM_MB = Math.round(TOTAL_MEM_MB * MAX_MEM_PERCENT / 100); console.log(`Password: ${PASSWORD ? 'SET' : 'DEFAULT'}`); console.log(`Limits: CPU ${MAX_CPU_PERCENT}%, Memory ${MAX_MEM_MB}MB / ${TOTAL_MEM_MB}MB`); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Track terminal PIDs for resource monitoring const terminals = {}; const terminalPids = new Set(); function getOrCreateTerminal(sessionId = 'main') { if (terminals[sessionId] && !terminals[sessionId].killed) { return terminals[sessionId]; } console.log(`Creating terminal: ${sessionId}`); const term = pty.spawn('/bin/bash', ['--login'], { name: 'xterm-256color', cols: 80, rows: 24, cwd: '/root', env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', LANG: 'C.UTF-8', HOME: '/root', SHELL: '/bin/bash' } }); // Track PID terminalPids.add(term.pid); term.outputBuffer = ''; term.maxBuffer = 50000; term.clients = new Set(); term.killed = false; term.onData((data) => { term.outputBuffer += data; if (term.outputBuffer.length > term.maxBuffer) { term.outputBuffer = term.outputBuffer.slice(-term.maxBuffer); } term.clients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { try { ws.send(JSON.stringify({ type: 'output', data })); } catch {} } }); }); term.onExit(() => { console.log(`Terminal ${sessionId} exited`); term.killed = true; terminalPids.delete(term.pid); const oldClients = new Set(term.clients); setTimeout(() => { delete terminals[sessionId]; const newTerm = getOrCreateTerminal(sessionId); oldClients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { newTerm.clients.add(ws); ws._term = newTerm; ws.send(JSON.stringify({ type: 'output', data: '\r\n\x1b[33m[Session restarted]\x1b[0m\r\n' })); } }); }, 500); }); terminals[sessionId] = term; return term; } // Pre-create terminal getOrCreateTerminal('main'); function validToken(token) { return token && token.trim() === PASSWORD.trim(); } app.post('/api/auth', (req, res) => { const { password } = req.body; if (password && password.trim() === PASSWORD.trim()) { return res.json({ success: true, token: PASSWORD.trim() }); } res.status(401).json({ error: 'Invalid password' }); }); app.get('/api/auth/check', (req, res) => { res.json({ authenticated: validToken(req.query.token) }); }); // Get resource usage for terminal processes only function getTerminalResourceUsage() { let cpuPercent = 0; let memMB = 0; try { // Get Node.js process memory const nodeMemMB = Math.round(process.memoryUsage().rss / 1024 / 1024); memMB += nodeMemMB; // Get child process stats terminalPids.forEach(pid => { try { // Get process stats const stat = execSync(`ps -p ${pid} -o %cpu,%mem --no-headers 2>/dev/null || true`, { encoding: 'utf8', timeout: 1000 }); const parts = stat.trim().split(/\s+/); if (parts.length >= 2) { cpuPercent += parseFloat(parts[0]) || 0; const memPercent = parseFloat(parts[1]) || 0; memMB += Math.round((memPercent / 100) * TOTAL_MEM_MB); } // Also get child processes const children = execSync(`pgrep -P ${pid} 2>/dev/null || true`, { encoding: 'utf8', timeout: 1000 }); children.trim().split('\n').filter(p => p).forEach(childPid => { try { const childStat = execSync(`ps -p ${childPid} -o %cpu,%mem --no-headers 2>/dev/null || true`, { encoding: 'utf8', timeout: 1000 }); const childParts = childStat.trim().split(/\s+/); if (childParts.length >= 2) { cpuPercent += parseFloat(childParts[0]) || 0; const childMemPercent = parseFloat(childParts[1]) || 0; memMB += Math.round((childMemPercent / 100) * TOTAL_MEM_MB); } } catch {} }); } catch {} }); } catch (e) { // Fallback to basic stats cpuPercent = Math.round(os.loadavg()[0] * 50); // Rough estimate memMB = Math.round(process.memoryUsage().rss / 1024 / 1024); } // Cap at limits cpuPercent = Math.min(Math.round(cpuPercent), MAX_CPU_PERCENT); const memPercent = Math.min(Math.round((memMB / MAX_MEM_MB) * 100), 100); return { cpu: { percent: cpuPercent, limit: MAX_CPU_PERCENT }, memory: { used: memMB, limit: MAX_MEM_MB, total: TOTAL_MEM_MB, percent: memPercent } }; } // Check and enforce resource limits function enforceResourceLimits() { const usage = getTerminalResourceUsage(); // If approaching limits, try to free resources if (usage.memory.percent > 90) { console.log(`[Enforcer] Memory at ${usage.memory.percent}% - triggering GC`); if (global.gc) { try { global.gc(); } catch {} } // Clear old terminal buffers Object.values(terminals).forEach(term => { if (term.outputBuffer.length > 10000) { term.outputBuffer = term.outputBuffer.slice(-10000); } }); } return usage; } app.get('/api/stats', (req, res) => { if (!validToken(req.query.token)) return res.status(401).json({}); res.json(enforceResourceLimits()); }); server.on('upgrade', (req, socket, head) => { const parsed = url.parse(req.url, true); if (!validToken(parsed.query.token)) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } wss.handleUpgrade(req, socket, head, ws => wss.emit('connection', ws, req)); }); wss.on('connection', (ws, req) => { const sessionId = url.parse(req.url, true).query.session || 'main'; const term = getOrCreateTerminal(sessionId); term.clients.add(ws); ws._term = term; ws.isAlive = true; if (term.outputBuffer) { ws.send(JSON.stringify({ type: 'output', data: term.outputBuffer })); } ws.send(JSON.stringify({ type: 'connected', session: sessionId, cols: term.cols, rows: term.rows })); ws.on('message', raw => { try { const msg = JSON.parse(raw.toString()); if (msg.type === 'input' && ws._term && !ws._term.killed) { ws._term.write(msg.data); } else if (msg.type === 'resize' && ws._term && !ws._term.killed) { try { ws._term.resize( Math.max(2, Math.min(500, +msg.cols || 80)), Math.max(2, Math.min(200, +msg.rows || 24)) ); } catch {} } else if (msg.type === 'ping') { ws.isAlive = true; ws.send(JSON.stringify({ type: 'pong' })); } } catch {} }); ws.on('close', () => { if (ws._term) ws._term.clients.delete(ws); }); ws.on('error', () => { if (ws._term) ws._term.clients.delete(ws); }); ws.on('pong', () => { ws.isAlive = true; }); }); // Heartbeat + resource check setInterval(() => { wss.clients.forEach(ws => { if (!ws.isAlive) { if (ws._term) ws._term.clients.delete(ws); return ws.terminate(); } ws.isAlive = false; ws.ping(); }); // Periodic resource enforcement enforceResourceLimits(); }, 30000); // More frequent resource check setInterval(enforceResourceLimits, 10000); app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); server.listen(PORT, '0.0.0.0', () => console.log(`Server running on port ${PORT}`)); process.on('SIGTERM', () => { Object.values(terminals).forEach(t => { if (!t.killed) t.kill(); }); process.exit(0); }); process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err.message); }); process.on('unhandledRejection', (err) => { console.error('Unhandled rejection:', err); });