import express from "express"; import session from "express-session"; import bcrypt from "bcrypt"; import helmet from "helmet"; import { createServer } from "http"; import { Server as IOServer } from "socket.io"; import path from "path"; import { fileURLToPath } from "url"; import dotenv from "dotenv"; import os from "os"; import { spawn, exec } from "child_process"; import { promisify } from "util"; import fs from "fs/promises"; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const execAsync = promisify(exec); const app = express(); const httpServer = createServer(app); const io = new IOServer(httpServer, { cors: { origin: process.env.NODE_ENV === "production" ? false : ["http://localhost:3000"], methods: ["GET", "POST"] } }); const PORT = process.env.PORT || 3000; const SESSION_SECRET = process.env.SESSION_SECRET || "devsecret"; const PASSWORD_HASH = process.env.WEBTERM_PASSWORD_HASH; // Platform detection const isWindows = process.platform === "win32"; const isLinux = process.platform === "linux"; const isMacOS = process.platform === "darwin"; // === System Utilities === class SystemMonitor { constructor() { this.cpuHistory = []; this.networkHistory = []; this.previousNetworkStats = null; } async getCPUUsage() { return new Promise((resolve) => { const startMeasure = this.cpuAverage(); setTimeout(() => { const endMeasure = this.cpuAverage(); const idleDifference = endMeasure.idle - startMeasure.idle; const totalDifference = endMeasure.total - startMeasure.total; const percentageCPU = 100 - Math.floor(100 * idleDifference / totalDifference); resolve(Math.max(0, Math.min(100, percentageCPU))); }, 100); }); } cpuAverage() { const cpus = os.cpus(); let user = 0, nice = 0, sys = 0, idle = 0, irq = 0; for (let cpu of cpus) { user += cpu.times.user; nice += cpu.times.nice || 0; sys += cpu.times.sys; irq += cpu.times.irq || 0; idle += cpu.times.idle; } return { idle: idle / cpus.length, total: (user + nice + sys + idle + irq) / cpus.length }; } async getMemoryInfo() { const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; return { total: Math.round(totalMem / 1024 / 1024), // MB used: Math.round(usedMem / 1024 / 1024), // MB free: Math.round(freeMem / 1024 / 1024), // MB percentage: Math.round((usedMem / totalMem) * 100) }; } async getDiskUsage() { try { if (isWindows) { const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption /format:csv'); const lines = stdout.split('\n').filter(line => line.includes(':')); if (lines.length > 0) { const parts = lines[0].split(','); if (parts.length >= 3) { const freeSpace = parseInt(parts[1]) || 0; const totalSpace = parseInt(parts[2]) || 1; const usedSpace = totalSpace - freeSpace; const percentage = Math.round((usedSpace / totalSpace) * 100); return { total: Math.round(totalSpace / 1024 / 1024 / 1024), // GB used: Math.round(usedSpace / 1024 / 1024 / 1024), // GB free: Math.round(freeSpace / 1024 / 1024 / 1024), // GB percentage: percentage }; } } } else { // Linux/macOS const { stdout } = await execAsync('df -h / | tail -1'); const parts = stdout.trim().split(/\s+/); if (parts.length >= 5) { const percentage = parseInt(parts[4].replace('%', '')) || 0; return { total: parts[1], used: parts[2], free: parts[3], percentage: percentage }; } } } catch (error) { console.warn('Disk usage detection failed:', error.message); } return { total: 0, used: 0, free: 0, percentage: 0 }; } async getProcessList() { try { let processes = []; if (isWindows) { const { stdout } = await execAsync( 'powershell "Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, CPU | ConvertTo-Csv -NoTypeInformation"' ); const lines = stdout.split('\n').slice(1).filter(line => line.trim()); processes = lines.map(line => { const [name, cpu] = line.replace(/"/g, '').split(','); return { name: name || 'Unknown', cpu: parseFloat(cpu) || 0 }; }).filter(p => p.name !== 'Unknown').slice(0, 8); } else { // Linux/macOS - using ps command const { stdout } = await execAsync('ps aux --sort=-%cpu | head -9 | tail -8'); const lines = stdout.split('\n').filter(line => line.trim()); processes = lines.map(line => { const parts = line.trim().split(/\s+/); return { name: parts[10] ? path.basename(parts[10]) : 'unknown', cpu: parseFloat(parts[2]) || 0 }; }); } return processes; } catch (error) { console.warn('Process list detection failed:', error.message); return []; } } async getNetworkStats() { try { if (isWindows) { // Jalankan PowerShell dengan benar const { stdout } = await execAsync( 'powershell -Command "Get-NetAdapterStatistics | Select-Object -Property ReceivedBytes, SentBytes | ConvertTo-Json"' ); // Ambil total RX dan TX dari semua adapter const stats = JSON.parse(stdout); let totalRx = 0, totalTx = 0; if (Array.isArray(stats)) { stats.forEach(s => { totalRx += parseInt(s.ReceivedBytes) || 0; totalTx += parseInt(s.SentBytes) || 0; }); } else if (stats) { totalRx = parseInt(stats.ReceivedBytes) || 0; totalTx = parseInt(stats.SentBytes) || 0; } // Hitung delta per detik (seperti di Linux) if (this.previousNetworkStats) { const rxSpeed = Math.max(0, totalRx - this.previousNetworkStats.rx); const txSpeed = Math.max(0, totalTx - this.previousNetworkStats.tx); this.previousNetworkStats = { rx: totalRx, tx: totalTx }; return { rx: rxSpeed, tx: txSpeed }; } else { this.previousNetworkStats = { rx: totalRx, tx: totalTx }; return { rx: 0, tx: 0 }; } } else { // Linux - read from /proc/net/dev const data = await fs.readFile('/proc/net/dev', 'utf8'); const lines = data.split('\n'); let totalRx = 0, totalTx = 0; for (let line of lines) { if (line.includes(':') && !line.includes('lo:')) { const parts = line.split(':')[1].trim().split(/\s+/); totalRx += parseInt(parts[0]) || 0; totalTx += parseInt(parts[8]) || 0; } } if (this.previousNetworkStats) { const rxSpeed = Math.max(0, totalRx - this.previousNetworkStats.rx); const txSpeed = Math.max(0, totalTx - this.previousNetworkStats.tx); this.previousNetworkStats = { rx: totalRx, tx: totalTx }; return { rx: rxSpeed, tx: txSpeed }; } else { this.previousNetworkStats = { rx: totalRx, tx: totalTx }; return { rx: 0, tx: 0 }; } } } catch (error) { console.warn('Network stats detection failed:', error.message); return { rx: 0, tx: 0 }; } } formatUptime(seconds) { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (days > 0) return `${days}d ${hours}h ${minutes}m`; if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; } async getSystemInfo() { try { const [cpuUsage, memInfo, diskInfo, processes, networkStats] = await Promise.all([ this.getCPUUsage(), this.getMemoryInfo(), this.getDiskUsage(), this.getProcessList(), this.getNetworkStats() ]); const cpuInfo = os.cpus()[0]; const uptime = os.uptime(); return { // System info platform: os.platform(), release: os.release(), arch: os.arch(), hostname: os.hostname(), uptime: this.formatUptime(uptime), // CPU info cpu_model: cpuInfo.model, cpu_cores: os.cpus().length, cpu_usage: cpuUsage, // Memory info total_mem: memInfo.total, used_mem: memInfo.used, free_mem: memInfo.free, mem_usage: memInfo.percentage, // Disk info disk_total: diskInfo.total, disk_used: diskInfo.used, disk_free: diskInfo.free, disk_usage: diskInfo.percentage, // Network info (speeds in bytes per second) network_rx: networkStats.rx, network_tx: networkStats.tx, // Process list processes: processes, // Load average (Linux/macOS only) loadavg: isWindows ? [0, 0, 0] : os.loadavg(), // User info user_info: os.userInfo(), timestamp: Date.now() }; } catch (error) { console.error('System info error:', error); return null; } } } const systemMonitor = new SystemMonitor(); // === Middleware === app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: [ "'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net" ], styleSrc: [ "'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://fonts.googleapis.com" ], fontSrc: [ "'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com" ], imgSrc: ["'self'", "data:"], connectSrc: ["'self'", "ws:", "wss:"] } }, crossOriginEmbedderPolicy: false })); app.use(express.urlencoded({ extended: false })); app.use(express.json()); app.use(express.static(path.join(__dirname, "public"))); app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); // === Session === app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 24 * 60 * 60 * 1000 // 24 hours }, }) ); // === Auth Middleware === function requireAuth(req, res, next) { if (req.session?.authed) return next(); return res.redirect("/login"); } // === Routes === app.get("/login", (req, res) => { if (req.session?.authed) return res.redirect("/"); res.render("login", { error: null }); }); app.post("/login", async (req, res) => { const { password } = req.body; try { if (!PASSWORD_HASH) { console.error("PASSWORD_HASH not set in environment variables"); return res.render("login", { error: "Server configuration error" }); } const isValid = await bcrypt.compare(password || "", PASSWORD_HASH); if (isValid) { req.session.authed = true; return res.redirect("/"); } else { return res.render("login", { error: "Invalid password" }); } } catch (error) { console.error("Login error:", error); return res.render("login", { error: "Authentication error" }); } }); app.get("/logout", (req, res) => { req.session.destroy(() => res.redirect("/login")); }); app.get("/", requireAuth, (req, res) => { res.render("terminal"); }); app.get("/sysinfo", requireAuth, async (req, res) => { try { const systemInfo = await systemMonitor.getSystemInfo(); if (systemInfo) { res.json(systemInfo); } else { res.status(500).json({ error: "Failed to fetch system information" }); } } catch (error) { console.error("System info endpoint error:", error); res.status(500).json({ error: "Internal server error" }); } }); // === Terminal Class for better process management === class Terminal { constructor(socket) { this.socket = socket; this.process = null; this.isActive = true; } async initialize() { try { // Try to use node-pty first const nodePty = await import("node-pty"); return this.initWithPty(nodePty); } catch (error) { console.warn("node-pty not available, using fallback:", error.message); return this.initWithSpawn(); } } initWithPty(nodePty) { const shell = this.getShell(); const env = { ...process.env }; // Set better environment for cross-platform compatibility if (isWindows) { env.TERM = 'windows-ansi'; } else { env.TERM = 'xterm-256color'; env.COLORTERM = 'truecolor'; } this.process = nodePty.spawn(shell, [], { name: isWindows ? 'windows-ansi' : 'xterm-256color', cols: 80, rows: 24, cwd: this.getInitialDirectory(), env: env, useConpty: isWindows, // Use ConPTY on Windows 10+ }); this.process.onData(data => { if (this.isActive) { this.socket.emit("terminal_output", data); } }); this.process.onExit(() => { if (this.isActive) { this.socket.emit("terminal_output", "\r\n\x1b[91mTerminal session ended\x1b[0m\r\n"); } }); // Socket event handlers this.socket.on("terminal_input", (data) => { if (this.process && this.isActive) { this.process.write(data); } }); this.socket.on("terminal_resize", ({ cols, rows }) => { if (this.process && this.isActive) { this.process.resize(cols || 80, rows || 24); } }); console.log(`Terminal initialized with node-pty (${shell})`); return true; } initWithSpawn() { const shell = this.getShell(); const args = this.getShellArgs(); this.process = spawn(shell, args, { cwd: this.getInitialDirectory(), env: { ...process.env, TERM: isWindows ? 'windows-ansi' : 'xterm-256color' }, stdio: ['pipe', 'pipe', 'pipe'] }); this.process.stdout.on("data", (data) => { if (this.isActive) { this.socket.emit("terminal_output", data.toString()); } }); this.process.stderr.on("data", (data) => { if (this.isActive) { this.socket.emit("terminal_output", data.toString()); } }); this.process.on("exit", (code) => { if (this.isActive) { this.socket.emit("terminal_output", `\r\n\x1b[91mProcess exited with code ${code}\x1b[0m\r\n`); } }); this.socket.on("terminal_input", (data) => { if (this.process && this.process.stdin && this.isActive) { this.process.stdin.write(data); } }); console.log(`Terminal initialized with spawn fallback (${shell})`); return true; } getShell() { if (isWindows) { return process.env.COMSPEC || "cmd.exe"; } else { return process.env.SHELL || "/bin/bash"; } } getShellArgs() { if (isWindows) { return []; } else { return ["-l"]; // Login shell } } getInitialDirectory() { let dir; if (isWindows) { dir = process.env.USERPROFILE || process.cwd(); } else { dir = process.env.HOME || process.cwd(); } try { require('fs').accessSync(dir, require('fs').constants.R_OK | require('fs').constants.X_OK); return dir; } catch (e) { console.warn(`Initial directory "${dir}" not accessible, fallback to process.cwd()`); return process.cwd(); } } destroy() { this.isActive = false; if (this.process) { try { if (typeof this.process.kill === 'function') { this.process.kill(); } else if (typeof this.process.destroy === 'function') { this.process.destroy(); } } catch (error) { console.warn("Error destroying terminal process:", error.message); } } } } // === Socket.IO Connection Handler === io.on("connection", async (socket) => { console.log(`Socket connected: ${socket.id} from ${socket.handshake.address}`); // Create terminal instance const terminal = new Terminal(socket); const initSuccess = await terminal.initialize(); if (!initSuccess) { socket.emit("terminal_output", "\r\n\x1b[91mFailed to initialize terminal\x1b[0m\r\n"); } // Send welcome message const welcomeMessage = `\x1b[36mWelcome to Web Terminal\x1b[0m\r\n` + `\x1b[90mPlatform:\x1b[0m ${process.platform} (${process.arch})\r\n` + `\x1b[90mShell:\x1b[0m ${terminal.getShell()}\r\n` + `\x1b[90mType 'help' or 'exit' as needed.\x1b[0m\r\n\r\n`; socket.emit("terminal_output", welcomeMessage); // Real-time system monitoring const systemInfoInterval = setInterval(async () => { try { const systemInfo = await systemMonitor.getSystemInfo(); if (systemInfo && socket.connected) { socket.emit("system_info", systemInfo); } } catch (error) { console.error("System monitoring error:", error); } }, 2000); // Cleanup on disconnect socket.on("disconnect", () => { console.log(`Socket disconnected: ${socket.id}`); clearInterval(systemInfoInterval); terminal.destroy(); }); // Error handling socket.on("error", (error) => { console.error(`Socket error for ${socket.id}:`, error); }); }); // === Error Handling === process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); }); // === Graceful Shutdown === // === Start Server === httpServer.listen(PORT, () => { console.log(`✅ Web Terminal Server running on http://localhost:${PORT}`); console.log(`📊 Platform: ${process.platform} (${process.arch})`); console.log(`🐚 Default Shell: ${isWindows ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL || "/bin/bash"}`); console.log(`🔐 Authentication: ${PASSWORD_HASH ? 'Enabled' : 'Disabled (set WEBTERM_PASSWORD_HASH)'}`); }); export default app;