← back to Port Viewer
server.js
973 lines
const http = require('http');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const nodemailer = require('nodemailer');
const crypto = require('crypto');
const PORT = 8888; // Permanent port assignment
const SERVER_IP = '45.61.58.125'; // Server public IP address
const SYSTEM_PORTS = [22, 53, 80, 443, 111, 123, 514]; // Common system ports to exclude
// Authentication
const AUTH_PASSWORD = 'serveraccess911'; // Change this to your desired password
const sessions = new Map(); // Store active sessions
// Store directory/port mappings in memory
const directoryData = {};
// Store previous port states for change detection
const previousPortStates = {};
// Email configuration
let emailConfig = {
enabled: true,
recipient: 'steve@designerwallcoverings.com',
smtp: {
host: 'localhost',
port: 25,
secure: false
}
};
// Create email transporter
let transporter = null;
try {
transporter = nodemailer.createTransport(emailConfig.smtp);
} catch (err) {
console.error('Failed to create email transporter:', err.message);
}
// Helper functions for authentication
function generateToken() {
return crypto.randomBytes(32).toString('hex');
}
function isAuthenticated(req) {
const authHeader = req.headers['authorization'];
if (!authHeader) return false;
const token = authHeader.replace('Bearer ', '');
return sessions.has(token);
}
function getCookie(req, name) {
const cookies = req.headers.cookie;
if (!cookies) return null;
const cookie = cookies.split(';').find(c => c.trim().startsWith(name + '='));
return cookie ? cookie.split('=')[1] : null;
}
const server = http.createServer((req, res) => {
// Login endpoint - always accessible
if (req.url === '/api/login' && req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const { password } = JSON.parse(body);
if (password === AUTH_PASSWORD) {
const token = generateToken();
sessions.set(token, { createdAt: Date.now() });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, token }));
} else {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Invalid password' }));
}
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request' }));
}
});
return;
}
// Logout endpoint
if (req.url === '/api/logout' && req.method === 'POST') {
const authHeader = req.headers['authorization'];
if (authHeader) {
const token = authHeader.replace('Bearer ', '');
sessions.delete(token);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
return;
}
// Check authentication for API routes (except login, server-info, suggest-port, domains, and proxy)
if (req.url.startsWith('/api/') && req.url !== '/api/server-info' && req.url !== '/api/suggest-port' && req.url !== '/api/domains' && !req.url.startsWith('/api/proxy/') && !isAuthenticated(req)) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unauthorized' }));
return;
}
if (req.url === '/api/server-info') {
// Return server information
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
serverIp: SERVER_IP,
port: PORT
}));
return;
}
if (req.url === '/api/ports') {
exec('ss -tulpn 2>/dev/null | grep LISTEN', (error, stdout, stderr) => {
if (error && !stdout) {
// Fallback to netstat if ss fails
exec('netstat -tulpn 2>/dev/null | grep LISTEN', (error2, stdout2, stderr2) => {
if (error2 && !stdout2) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to fetch port information' }));
return;
}
enrichPortData(parseNetstat(stdout2), (enrichedPorts) => {
const allServices = mergeWithOfflineDirectories(enrichedPorts);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(allServices));
});
});
} else {
enrichPortData(parseSS(stdout), (enrichedPorts) => {
const allServices = mergeWithOfflineDirectories(enrichedPorts);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(allServices));
});
}
});
} else if (req.url.startsWith('/api/restart/')) {
// Handle restart requests
const directory = decodeURIComponent(req.url.replace('/api/restart/', ''));
handleRestart(directory, res);
} else if (req.url === '/api/directories') {
// Get stored directory data
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(directoryData));
} else if (req.url === '/api/domains') {
// Get all domains from nginx configs
exec('grep -h "server_name" /etc/nginx/sites-enabled/* 2>/dev/null', (error, stdout) => {
const domains = [];
if (stdout) {
const lines = stdout.trim().split('\n');
lines.forEach(line => {
// Parse server_name lines
const match = line.match(/server_name\s+([^;]+);/);
if (match) {
const serverNames = match[1].trim().split(/\s+/);
serverNames.forEach(domain => {
if (domain && !domain.includes('localhost') && !domain.match(/^\d+\.\d+\.\d+\.\d+$/)) {
domains.push(domain);
}
});
}
});
}
// Get unique domains
const uniqueDomains = [...new Set(domains)].sort();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ domains: uniqueDomains }));
});
} else if (req.url === '/api/suggest-port') {
// Suggest an available port
exec('ss -tulpn | grep LISTEN', (error, stdout) => {
const usedPorts = new Set();
// Parse all listening ports
const lines = stdout.trim().split('\n');
lines.forEach(line => {
const match = line.match(/:(\d+)\s/);
if (match) {
usedPorts.add(parseInt(match[1]));
}
});
// Find available port starting from 9000
let suggestedPort = 9000;
while (usedPorts.has(suggestedPort)) {
suggestedPort++;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ port: suggestedPort }));
});
} else if (req.url === '/api/scan-directories') {
// Scan for directories with package.json - deep scan in specific directories
const scanPaths = [
'find /root/DW-Websites -name "package.json" -type f 2>/dev/null',
'find /root/WebsitesMisc -name "package.json" -type f 2>/dev/null',
'find /root/Scripts -name "package.json" -type f 2>/dev/null',
'find /opt -name "package.json" -type f 2>/dev/null',
'find /root -maxdepth 3 -name "package.json" -type f 2>/dev/null'
];
const combinedCommand = scanPaths.join(' ; ');
exec(combinedCommand, (error, stdout) => {
const directories = new Set(); // Use Set to avoid duplicates
if (stdout) {
const files = stdout.trim().split('\n');
files.forEach(file => {
if (file) {
const dir = path.dirname(file);
// Exclude node_modules
if (!dir.includes('node_modules')) {
directories.add(dir);
}
}
});
}
const uniqueDirectories = Array.from(directories).sort();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
directories: uniqueDirectories,
count: uniqueDirectories.length
}));
});
} else if (req.url === '/api/add-directory' && req.method === 'POST') {
// Handle adding a directory manually
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const { directory, port, appName } = JSON.parse(body);
if (!directory) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory path is required' }));
return;
}
// Check if directory exists
if (!fs.existsSync(directory)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory does not exist' }));
return;
}
// Add to directoryData
directoryData[directory] = {
lastPort: port || 'N/A',
lastSeen: new Date().toISOString(),
process: 'Unknown',
appName: appName || path.basename(directory)
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Directory added successfully',
directory: directory
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request body' }));
}
});
} else if (req.url === '/api/ask' && req.method === 'POST') {
// Handle natural language server queries
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const { query } = JSON.parse(body);
handleServerQuery(query, res);
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request body' }));
}
});
} else if (req.url.startsWith('/api/delete-directory/')) {
// Handle deleting a directory
const directory = decodeURIComponent(req.url.replace('/api/delete-directory/', ''));
if (directoryData[directory]) {
delete directoryData[directory];
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Directory removed successfully'
}));
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory not found' }));
}
} else if (req.url.startsWith('/api/stop-port/') && req.method === 'POST') {
// Stop/kill process on port
const port = decodeURIComponent(req.url.replace('/api/stop-port/', ''));
exec(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, (error, stdout, stderr) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: `Stopped process on port ${port}`
}));
});
} else if (req.url === '/api/assign-port' && req.method === 'POST') {
// Assign port to directory
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const { directory, port, appName } = JSON.parse(body);
if (!directory || !port) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory and port are required' }));
return;
}
// Update or create directory mapping
directoryData[directory] = {
lastPort: port,
lastSeen: new Date().toISOString(),
process: 'Unknown',
appName: appName || path.basename(directory)
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: `Assigned port ${port} to ${directory}`,
directory: directory,
port: port
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request body' }));
}
});
} else if (req.url === '/api/email-config') {
// Get email configuration
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
enabled: emailConfig.enabled,
recipient: emailConfig.recipient
}));
} else if (req.url === '/api/email-config' && req.method === 'POST') {
// Update email configuration
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const { enabled, recipient } = JSON.parse(body);
if (enabled !== undefined) emailConfig.enabled = enabled;
if (recipient) emailConfig.recipient = recipient;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Email configuration updated',
config: {
enabled: emailConfig.enabled,
recipient: emailConfig.recipient
}
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request body' }));
}
});
} else if (req.url.startsWith('/api/proxy/')) {
// Proxy endpoint to bypass X-Frame-Options
const targetUrl = decodeURIComponent(req.url.replace('/api/proxy/', ''));
exec(`curl -sL "${targetUrl}"`, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
if (error) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end(`<html><body style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;background:#f0f0f0;"><div style="text-align:center;"><h2>❌ Failed to load</h2><p>${targetUrl}</p><p style="font-size:0.9em;color:#666;">${error.message}</p></div></body></html>`);
return;
}
res.writeHead(200, {
'Content-Type': 'text/html',
'X-Frame-Options': 'ALLOWALL',
'Content-Security-Policy': ''
});
res.end(stdout);
});
} else if (req.url === '/test') {
// Serve test page
const filePath = path.join(__dirname, 'test.html');
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
});
} else {
// Serve index.html
const filePath = path.join(__dirname, 'public', 'index.html');
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
});
}
});
function parseSS(output) {
const lines = output.trim().split('\n');
const ports = [];
lines.forEach(line => {
const parts = line.trim().split(/\s+/);
if (parts.length < 5) return;
const protocol = parts[0];
const localAddress = parts[4];
const [address, port] = localAddress.split(':').slice(-2);
// Extract process info and PID
let process = 'Unknown';
let pid = null;
const usersMatch = line.match(/users:\(\("([^"]+)",pid=(\d+)/);
if (usersMatch) {
process = usersMatch[1];
pid = usersMatch[2];
}
ports.push({
port: port || 'N/A',
protocol: protocol.toUpperCase(),
address: address === '*' ? '0.0.0.0' : address,
process: process,
pid: pid
});
});
return ports.sort((a, b) => parseInt(a.port) - parseInt(b.port));
}
function parseNetstat(output) {
const lines = output.trim().split('\n');
const ports = [];
lines.forEach(line => {
const parts = line.trim().split(/\s+/);
if (parts.length < 7) return;
const protocol = parts[0];
const localAddress = parts[3];
const [address, port] = localAddress.split(':').slice(-2);
const processInfo = parts[6] || 'Unknown';
// Extract PID from process info (format: pid/process)
let process = processInfo;
let pid = null;
const pidMatch = processInfo.match(/(\d+)\/(.*)/);
if (pidMatch) {
pid = pidMatch[1];
process = pidMatch[2];
}
ports.push({
port: port || 'N/A',
protocol: protocol.toUpperCase(),
address: address === '*' ? '0.0.0.0' : address,
process: process,
pid: pid
});
});
return ports.sort((a, b) => parseInt(a.port) - parseInt(b.port));
}
function enrichPortData(ports, callback) {
// Get nginx config data - read actual config files
exec('cat /etc/nginx/sites-enabled/* 2>/dev/null', (error, nginxOutput) => {
const portMapping = parseNginxConfig(nginxOutput || '');
// Get Docker data
exec('docker ps --format "{{.Names}}|{{.Ports}}|{{.Image}}"', (error, dockerOutput) => {
const dockerMapping = parseDockerPs(dockerOutput || '');
// Get directory info for each port with PID
let completed = 0;
const enrichedPorts = ports.map(portInfo => {
const portNum = portInfo.port;
const websites = portMapping[portNum] || [];
const docker = dockerMapping[portNum] || null;
return {
...portInfo,
websites: websites,
website: websites.length > 0 ? websites.join(', ') : null,
docker: docker,
appName: getAppName(portInfo, websites, docker),
directory: null,
isUserApp: false
};
});
// Get directory for each port
enrichedPorts.forEach((portInfo, index) => {
if (portInfo.pid && !SYSTEM_PORTS.includes(parseInt(portInfo.port))) {
getProcessDirectory(portInfo.pid, (dir) => {
enrichedPorts[index].directory = dir;
enrichedPorts[index].isUserApp = dir && !dir.startsWith('/usr') && !dir.startsWith('/lib') && !dir.startsWith('/bin');
// Store directory data
if (dir && enrichedPorts[index].isUserApp) {
directoryData[dir] = {
lastPort: portInfo.port,
lastSeen: new Date().toISOString(),
process: portInfo.process,
appName: portInfo.appName
};
}
completed++;
if (completed === enrichedPorts.length) {
callback(enrichedPorts);
}
});
} else {
completed++;
if (completed === enrichedPorts.length) {
callback(enrichedPorts);
}
}
});
// If no async operations, call immediately
if (enrichedPorts.length === 0 || enrichedPorts.every(p => !p.pid || SYSTEM_PORTS.includes(parseInt(p.port)))) {
callback(enrichedPorts);
}
});
});
}
function parseNginxConfig(output) {
const mapping = {};
// Split into server blocks
const serverBlocks = output.split(/server\s*{/);
serverBlocks.forEach(block => {
// Find server_name
const serverNameMatch = block.match(/server_name\s+([^;]+);/);
// Find proxy_pass with port
const proxyPassMatch = block.match(/proxy_pass\s+http:\/\/[^:]+:(\d+)/);
if (serverNameMatch && proxyPassMatch) {
const port = proxyPassMatch[1];
const domains = serverNameMatch[1]
.split(/\s+/)
.filter(d => d && d !== '_' && d !== 'www._' && !d.match(/^\d+\.\d+\.\d+\.\d+$/))
.map(d => d.replace(/^www\./, '')); // Remove www. prefix
if (domains.length > 0) {
if (!mapping[port]) mapping[port] = [];
domains.forEach(domain => {
if (!mapping[port].includes(domain)) {
mapping[port].push(domain);
}
});
}
}
});
return mapping;
}
function parseDockerPs(output) {
const lines = output.trim().split('\n');
const mapping = {};
lines.forEach(line => {
const [name, ports, image] = line.split('|');
if (!ports) return;
const portMatch = ports.match(/:(\d+)->/g);
if (portMatch) {
portMatch.forEach(pm => {
const port = pm.match(/:(\d+)->/)[1];
mapping[port] = { name, image };
});
}
});
return mapping;
}
function getAppName(portInfo, websites, docker) {
if (docker) return docker.name;
if (websites.length > 0) return websites[0];
// Guess based on port
const portNum = parseInt(portInfo.port);
if (portNum === 80 || portNum === 443) return 'Web Server (nginx)';
if (portNum === 22) return 'SSH Server';
if (portNum === 3000) return 'Node.js App';
if (portNum === 8080) return 'HTTP Server';
if (portNum === 9000) return 'Node.js App';
if (portInfo.process.includes('nginx')) return 'nginx';
if (portInfo.process.includes('node')) return 'Node.js App';
if (portInfo.process.includes('docker')) return 'Docker Service';
return portInfo.process;
}
function getProcessDirectory(pid, callback) {
exec(`readlink -f /proc/${pid}/cwd 2>/dev/null`, (error, stdout) => {
if (error || !stdout) {
callback(null);
} else {
callback(stdout.trim());
}
});
}
function mergeWithOfflineDirectories(activePorts) {
const activeDirectories = new Set();
const result = [...activePorts];
// Mark active directories
activePorts.forEach(port => {
if (port.directory && port.isUserApp) {
activeDirectories.add(port.directory);
}
});
// Add offline directories
Object.keys(directoryData).forEach(directory => {
if (!activeDirectories.has(directory)) {
const data = directoryData[directory];
result.push({
port: data.lastPort || 'N/A',
protocol: 'N/A',
address: 'N/A',
process: data.process || 'Unknown',
pid: null,
websites: [],
website: null,
docker: null,
appName: data.appName || path.basename(directory),
directory: directory,
isUserApp: true,
isOffline: true
});
}
});
return result;
}
function handleServerQuery(query, res) {
const q = query.toLowerCase().trim();
let command = '';
let type = 'general';
// Parse the query and determine the command
if (q === 'help' || q.includes('help')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
type: 'help',
response: `**Available Commands:**
- uptime / show uptime
- memory / memory usage / ram
- cpu / cpu usage / load
- disk / disk usage / storage
- ports / open ports / listening ports
- processes / top processes
- directories / my directories
- network / network info
- users / logged in users
- services / systemd services`
}));
return;
}
if (q.includes('uptime')) {
command = 'uptime -p';
type = 'uptime';
} else if (q.includes('memory') || q.includes('ram')) {
command = 'free -h';
type = 'memory';
} else if (q.includes('cpu') || q.includes('load')) {
command = 'top -bn1 | head -20';
type = 'cpu';
} else if (q.includes('disk') || q.includes('storage')) {
command = 'df -h';
type = 'disk';
} else if (q.includes('port') && (q.includes('open') || q.includes('listening'))) {
command = 'ss -tulpn | grep LISTEN';
type = 'ports';
} else if (q.includes('process') || q.includes('top')) {
command = 'ps aux --sort=-%mem | head -15';
type = 'processes';
} else if (q.includes('director')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
type: 'directories',
response: JSON.stringify(directoryData, null, 2),
data: directoryData
}));
return;
} else if (q.includes('network')) {
command = 'ip addr show';
type = 'network';
} else if (q.includes('user') || q.includes('logged in')) {
command = 'who';
type = 'users';
} else if (q.includes('service')) {
command = 'systemctl list-units --type=service --state=running';
type = 'services';
} else if (q.includes('docker')) {
command = 'docker ps';
type = 'docker';
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: 'I didn\'t understand that question. Type "help" to see available commands.'
}));
return;
}
// Execute the command
exec(command, (error, stdout, stderr) => {
if (error) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: stderr || error.message
}));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
type: type,
query: query,
response: stdout.trim()
}));
});
}
function handleRestart(directory, res) {
console.log(`[RESTART] Request for directory: ${directory}`);
if (!directory || !directoryData[directory]) {
console.log(`[RESTART] Directory not found: ${directory}`);
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory not found' }));
return;
}
const data = directoryData[directory];
const lastPort = data.lastPort;
console.log(`[RESTART] Port: ${lastPort}`);
// Find package.json or main entry point
const packageJsonPath = path.join(directory, 'package.json');
fs.readFile(packageJsonPath, 'utf8', (err, content) => {
let startCommand = 'npm start';
if (!err) {
try {
const pkg = JSON.parse(content);
if (pkg.scripts && pkg.scripts.start) {
startCommand = 'npm start';
} else if (pkg.main) {
startCommand = `node ${pkg.main}`;
}
} catch (e) {
// Fallback to npm start
}
}
// Kill existing process on the port first
exec(`lsof -ti:${lastPort} | xargs kill -9 2>/dev/null`, () => {
// Also kill any node processes running in this directory
exec(`ps aux | grep "node.*${directory}" | grep -v grep | awk '{print $2}' | xargs kill -9 2>/dev/null`, () => {
// Wait a bit for processes to be killed
setTimeout(() => {
// Start the process in the directory with PORT environment variable
const logFile = `/tmp/${path.basename(directory)}.log`;
// For Next.js apps, call next directly instead of through npm
let finalCommand = startCommand;
if (startCommand === 'npm start' || startCommand.includes('next')) {
// Check if package.json has next
const pkg = JSON.parse(content);
if (pkg.scripts && pkg.scripts.start && pkg.scripts.start.includes('next')) {
// Call next directly to avoid npm flag issues - use dev mode for instant start
finalCommand = `npx next dev -p ${lastPort}`;
} else {
finalCommand = `PORT=${lastPort} ${startCommand}`;
}
} else {
finalCommand = `PORT=${lastPort} ${startCommand}`;
}
// Use bash with NVM to ensure correct Node version
const bashCommand = `bash -c "source ~/.nvm/nvm.sh && cd '${directory}' && ${finalCommand} > ${logFile} 2>&1 &"`;
console.log(`[RESTART] Executing: ${finalCommand}`);
exec(bashCommand, (error, stdout, stderr) => {
if (error) {
console.log(`[RESTART] Error: ${error.message}`);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Failed to restart',
message: stderr || error.message,
logFile: logFile
}));
} else {
console.log(`[RESTART] Command executed successfully`);
// Respond immediately - don't wait
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: `Restarted ${path.basename(directory)} on port ${lastPort}`,
directory: directory,
lastPort: lastPort,
openUrl: `http://localhost:${lastPort}`,
logFile: logFile
}));
}
});
}, 500); // Reduced wait time
});
});
});
}
async function sendDownAlert(directory, port, appName) {
if (!emailConfig.enabled || !transporter) {
console.log(`[Alert] Would send email: ${appName} (${directory}) on port ${port} is DOWN`);
return;
}
const mailOptions = {
from: '"Port Viewer Alert" <noreply@server.local>',
to: emailConfig.recipient,
subject: `🚨 ALERT: ${appName || 'Application'} is DOWN`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #d63031; border-bottom: 3px solid #d63031; padding-bottom: 10px;">
🚨 Port Down Alert
</h2>
<div style="background: #fff3cd; padding: 20px; border-radius: 10px; margin: 20px 0;">
<h3 style="margin-top: 0;">Application Offline</h3>
<p><strong>Application:</strong> ${appName || 'Unknown'}</p>
<p><strong>Directory:</strong> <code>${directory}</code></p>
<p><strong>Port:</strong> ${port}</p>
<p><strong>Time:</strong> ${new Date().toLocaleString()}</p>
</div>
<p>The application has stopped responding and is no longer listening on its port.</p>
<p style="color: #666; font-size: 12px; margin-top: 30px;">
This is an automated alert from Port Viewer monitoring system.
</p>
</div>
`
};
try {
await transporter.sendMail(mailOptions);
console.log(`✉️ Email alert sent to ${emailConfig.recipient} for ${appName}`);
} catch (error) {
console.error(`Failed to send email alert:`, error.message);
}
}
function checkPortChanges(currentPorts) {
const currentState = {};
// Build current state map
currentPorts.forEach(port => {
if (port.directory && port.isUserApp) {
currentState[port.directory] = {
isOnline: !port.isOffline,
port: port.port,
appName: port.appName
};
}
});
// Check for directories that went offline
Object.keys(currentState).forEach(directory => {
const current = currentState[directory];
const previous = previousPortStates[directory];
// If we have previous state and it was online but now is offline
if (previous && previous.isOnline && !current.isOnline) {
console.log(`⚠️ ALERT: ${current.appName} (${directory}) went offline!`);
sendDownAlert(directory, current.port, current.appName);
}
});
// Also check for directories that completely disappeared
Object.keys(previousPortStates).forEach(directory => {
if (!currentState[directory] && previousPortStates[directory].isOnline) {
const prev = previousPortStates[directory];
console.log(`⚠️ ALERT: ${prev.appName} (${directory}) disappeared!`);
sendDownAlert(directory, prev.port, prev.appName);
}
});
// Update previous state
Object.keys(currentState).forEach(directory => {
previousPortStates[directory] = currentState[directory];
});
}
// Monitor ports every 60 seconds
setInterval(() => {
exec('ss -tulpn 2>/dev/null | grep LISTEN', (error, stdout, stderr) => {
if (error && !stdout) {
exec('netstat -tulpn 2>/dev/null | grep LISTEN', (error2, stdout2, stderr2) => {
if (!error2 || stdout2) {
enrichPortData(parseNetstat(stdout2), (enrichedPorts) => {
const allServices = mergeWithOfflineDirectories(enrichedPorts);
checkPortChanges(allServices);
});
}
});
} else {
enrichPortData(parseSS(stdout), (enrichedPorts) => {
const allServices = mergeWithOfflineDirectories(enrichedPorts);
checkPortChanges(allServices);
});
}
});
}, 60000); // Check every 60 seconds
server.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Port Viewer running at http://localhost:${PORT}`);
console.log(`📊 View all active ports in your browser`);
console.log(`📧 Email alerts enabled for: ${emailConfig.recipient}`);
console.log(`⏱️ Port monitoring: Every 60 seconds`);
console.log(`🌐 External access: http://${SERVER_IP}:${PORT}`);
});