UNPKG

web-terminal-server

Version:

Professional web-based terminal server with persistent sessions, live sharing, smart port detection, Cloudflare tunnels, and full CLI support

338 lines (287 loc) 14.7 kB
#!/usr/bin/env node const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); // Parse command line arguments const args = process.argv.slice(2); const flags = {}; for (let i = 0; i < args.length; i++) { if (args[i].startsWith('--')) { const flag = args[i].substring(2); const nextArg = args[i + 1]; if (nextArg && !nextArg.startsWith('--')) { flags[flag] = nextArg; i++; } else { flags[flag] = true; } } } // Display help if (flags.help || flags.h) { console.log(` ╔════════════════════════════════════════════════════════════════╗ ║ Web Terminal Server - Professional Edition ║ ╚════════════════════════════════════════════════════════════════╝ Usage: npx web-terminal-server [options] Options: --port <number> Port to run the server (default: 5000) --security <level> Security level: full, limited, restricted (default: full) --ngrok Enable ngrok tunnel (requires NGROK_AUTH_TOKEN in .env) --no-browser Don't open browser automatically --help Show this help message Examples: npx web-terminal-server npx web-terminal-server --port 3000 npx web-terminal-server --security limited --ngrok npx web-terminal-server --port 8080 --no-browser Security Levels: full - Complete system access (cd / works) limited - Access to home and temp directories restricted - Home directory only Environment Variables (.env): PORT Server port (default: 5000) TERMINAL_SECURITY Security level (default: full) BASE_URL Custom base URL for sharing NGROK_AUTH_TOKEN Ngrok authentication token For more info: https://github.com/OzlevyQ/web-terminal-server `); process.exit(0); } // Show startup banner console.log(` ╔════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██╗ ██╗███████╗██████╗ ████████╗███████╗██████╗ ║ ║ ██║ ██║██╔════╝██╔══██╗ ╚══██╔══╝██╔════╝██╔══██╗ ║ ║ ██║ █╗ ██║█████╗ ██████╔╝ ██║ █████╗ ██████╔╝ ║ ║ ██║███╗██║██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗ ║ ║ ╚███╔███╔╝███████╗██████╔╝ ██║ ███████╗██║ ██║ ║ ║ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ║ ║ ║ ║ Web Terminal Server - Professional Edition ║ ║ Persistent Sessions & Live Sharing ║ ╚════════════════════════════════════════════════════════════════╝ `); // Set environment variables from flags if (flags.port) process.env.TERMINAL_PORT = flags.port; if (flags.security) process.env.TERMINAL_SECURITY = flags.security; // Check for .env file in current directory const envPath = path.join(process.cwd(), '.env'); if (fs.existsSync(envPath)) { console.log('📁 Loading .env file from current directory...'); require('dotenv').config({ path: envPath }); } // Start the server const serverPath = path.join(__dirname, '..', 'src', 'server.js'); const serverProcess = spawn('node', [serverPath], { stdio: 'inherit', env: { ...process.env } }); // Handle ngrok if requested let ngrokUrl = null; if (flags.ngrok) { setTimeout(async () => { console.log('\n🌐 Starting ngrok tunnel...'); const port = process.env.TERMINAL_PORT || 5000; let connected = false; // Try using ngrok module first try { let ngrok; // Check if ngrok module is available try { ngrok = require('ngrok'); } catch (moduleError) { // Try to require from parent directories try { ngrok = require(path.join(process.cwd(), 'node_modules', 'ngrok')); } catch { // Module not found, try to install it console.log('📦 Ngrok module not found. Installing...'); const { execSync } = require('child_process'); try { execSync('npm install ngrok --no-save', { stdio: 'inherit', cwd: process.cwd() }); ngrok = require('ngrok'); } catch (installError) { console.log('\n⚠️ Could not install ngrok module automatically.'); throw new Error('Ngrok module not available'); } } } // Set timeout for ngrok connection const connectOptions = { addr: port }; // Only add authtoken if it exists if (process.env.NGROK_AUTH_TOKEN && process.env.NGROK_AUTH_TOKEN.trim()) { connectOptions.authtoken = process.env.NGROK_AUTH_TOKEN.trim(); } const connectPromise = ngrok.connect(connectOptions); // Add timeout to prevent hanging const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Ngrok connection timeout')), 10000); }); // Race between connection and timeout ngrokUrl = await Promise.race([connectPromise, timeoutPromise]); connected = true; console.log('\n╔════════════════════════════════════════════════════════════════╗'); console.log('║ 🌐 NGROK TUNNEL ACTIVE ║'); console.log('╚════════════════════════════════════════════════════════════════╝'); console.log(`\n🔗 Public URL: ${ngrokUrl}`); console.log('📋 Share this URL to access your terminal from anywhere!'); console.log('\n═══════════════════════════════════════════════════════════════'); // Update BASE_URL environment variable for the server process.env.BASE_URL = ngrokUrl; // Open ngrok URL in browser if (!flags['no-browser']) { setTimeout(() => { console.log(`\n🌐 Opening browser at: ${ngrokUrl}`); const platform = process.platform; const opener = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; spawn(opener, [ngrokUrl], { detached: true, stdio: 'ignore' }); }, 1000); } } catch (error) { console.log('⚠️ Ngrok module failed:', error.message); } // If module didn't work, try CLI as fallback if (!connected) { console.log('📱 Trying ngrok CLI...'); // Check if ngrok CLI exists const { execSync } = require('child_process'); let hasNgrokCli = false; try { execSync('ngrok version', { stdio: 'ignore' }); hasNgrokCli = true; } catch { console.log('\n⚠️ Ngrok CLI not found.'); console.log(' Install with: brew install ngrok (Mac)'); console.log(' Or download from: https://ngrok.com/download'); console.log('\n📌 Tip: Add NGROK_AUTH_TOKEN to your .env file for better experience'); return; } if (hasNgrokCli) { try { // Kill any existing ngrok processes first try { execSync('pkill ngrok', { stdio: 'ignore' }); await new Promise(resolve => setTimeout(resolve, 1000)); } catch {} // Start ngrok with output capture console.log('⏳ Starting ngrok tunnel...'); const ngrokProcess = spawn('ngrok', ['http', port.toString(), '--log=stdout'], { stdio: ['ignore', 'pipe', 'pipe'] }); let output = ''; let timeoutId; // Set up a promise that resolves when URL is found or timeout const urlPromise = new Promise((resolve) => { // Timeout after 10 seconds timeoutId = setTimeout(() => { resolve(null); }, 10000); // Listen for ngrok output ngrokProcess.stdout.on('data', (data) => { output += data.toString(); // Look for URL in ngrok output const urlMatch = output.match(/url=(https:\/\/[^\s]+)/); if (urlMatch) { clearTimeout(timeoutId); resolve(urlMatch[1]); } }); ngrokProcess.stderr.on('data', (data) => { const error = data.toString(); if (error.includes('ERROR')) { console.log('⚠️ Ngrok error:', error); } }); }); // Wait for URL or timeout ngrokUrl = await urlPromise; // If we didn't get URL from output, try API if (!ngrokUrl) { await new Promise(resolve => setTimeout(resolve, 2000)); try { const result = execSync('curl -s http://127.0.0.1:4040/api/tunnels', { encoding: 'utf8' }); const data = JSON.parse(result); if (data.tunnels && data.tunnels.length > 0) { const httpsTunnel = data.tunnels.find(t => t.proto === 'https') || data.tunnels[0]; ngrokUrl = httpsTunnel.public_url; } } catch (e) { // API failed, ngrok might still be starting } } if (ngrokUrl) { console.log('\n╔════════════════════════════════════════════════════════════════╗'); console.log('║ 🌐 NGROK TUNNEL ACTIVE ║'); console.log('╚════════════════════════════════════════════════════════════════╝'); console.log(`\n🔗 Public URL: ${ngrokUrl}`); console.log('📋 Share this URL to access your terminal from anywhere!'); console.log('\n📊 Ngrok Dashboard: http://127.0.0.1:4040'); console.log('═══════════════════════════════════════════════════════════════\n'); // Open URL in browser if (!flags['no-browser']) { setTimeout(() => { console.log(`🌐 Opening browser at: ${ngrokUrl}`); const platform = process.platform; const opener = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; spawn(opener, [ngrokUrl], { detached: true, stdio: 'ignore' }); }, 1000); } } else { // If we couldn't get the URL, show manual instructions console.log('\n╔════════════════════════════════════════════════════════════════╗'); console.log('║ 🌐 NGROK TUNNEL STARTED ║'); console.log('╚════════════════════════════════════════════════════════════════╝'); console.log('\n📌 Ngrok is running! To see your public URL:'); console.log(' 1. Open new terminal'); console.log(' 2. Run: ./check-ngrok.sh'); console.log(' OR'); console.log(' 3. Open browser: http://127.0.0.1:4040'); console.log('\n💡 Tip: The URL format is usually:'); console.log(` https://[random].ngrok-free.app`); console.log('═══════════════════════════════════════════════════════════════\n'); } } catch (error) { console.log('⚠️ Could not start ngrok. Error:', error.message); } } } }, 3000); // Give server time to start } // Open browser unless disabled (only if not using ngrok) if (!flags['no-browser'] && !flags.ngrok) { setTimeout(() => { const port = process.env.TERMINAL_PORT || 5000; const url = `http://localhost:${port}`; console.log(`\n🌐 Opening browser at ${url}`); const platform = process.platform; const opener = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; spawn(opener, [url], { detached: true, stdio: 'ignore' }); }, 3000); } // Handle graceful shutdown process.on('SIGINT', () => { console.log('\n\n👋 Shutting down Web Terminal Server...'); // Kill ngrok if running try { require('child_process').execSync('pkill ngrok', { stdio: 'ignore' }); } catch {} serverProcess.kill('SIGINT'); process.exit(0); }); process.on('SIGTERM', () => { serverProcess.kill('SIGTERM'); process.exit(0); }); serverProcess.on('exit', (code) => { process.exit(code); });