Files
ProxmoxVE-Local/server.js
Rögl-Brunner Michel 6ee5c5c017 PoC
2025-09-09 14:24:36 +02:00

245 lines
6.5 KiB
JavaScript

import { createServer } from 'http';
import { parse } from 'url';
import next from 'next';
import { WebSocketServer } from 'ws';
import { spawn } from 'child_process';
import { join, resolve } from 'path';
const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = process.env.PORT || 3000;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
// WebSocket handler for script execution
class ScriptExecutionHandler {
constructor(server) {
this.wss = new WebSocketServer({
server,
path: '/ws/script-execution'
});
this.activeExecutions = new Map();
this.setupWebSocket();
}
setupWebSocket() {
this.wss.on('connection', (ws, request) => {
console.log('New WebSocket connection for script execution');
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(ws, message);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
this.sendMessage(ws, {
type: 'error',
data: 'Invalid message format',
timestamp: Date.now()
});
}
});
ws.on('close', () => {
console.log('WebSocket connection closed');
this.cleanupActiveExecutions(ws);
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
this.cleanupActiveExecutions(ws);
});
});
}
async handleMessage(ws, message) {
const { action, scriptPath, executionId } = message;
switch (action) {
case 'start':
if (scriptPath && executionId) {
await this.startScriptExecution(ws, scriptPath, executionId);
} else {
this.sendMessage(ws, {
type: 'error',
data: 'Missing scriptPath or executionId',
timestamp: Date.now()
});
}
break;
case 'stop':
if (executionId) {
this.stopScriptExecution(executionId);
}
break;
default:
this.sendMessage(ws, {
type: 'error',
data: 'Unknown action',
timestamp: Date.now()
});
}
}
async startScriptExecution(ws, scriptPath, executionId) {
try {
// Basic validation
const scriptsDir = join(process.cwd(), 'scripts');
const resolvedPath = resolve(scriptPath);
if (!resolvedPath.startsWith(resolve(scriptsDir))) {
this.sendMessage(ws, {
type: 'error',
data: 'Script path is not within the allowed scripts directory',
timestamp: Date.now()
});
return;
}
// Check if execution is already running
if (this.activeExecutions.has(executionId)) {
this.sendMessage(ws, {
type: 'error',
data: 'Script execution already running',
timestamp: Date.now()
});
return;
}
// Start script execution
const process = spawn('bash', [scriptPath], {
cwd: scriptsDir,
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
// Store the execution
this.activeExecutions.set(executionId, { process, ws });
// Send start message
this.sendMessage(ws, {
type: 'start',
data: `Starting execution of ${scriptPath}`,
timestamp: Date.now()
});
// Handle stdout
process.stdout?.on('data', (data) => {
this.sendMessage(ws, {
type: 'output',
data: data.toString(),
timestamp: Date.now()
});
});
// Handle stderr
process.stderr?.on('data', (data) => {
this.sendMessage(ws, {
type: 'error',
data: data.toString(),
timestamp: Date.now()
});
});
// Handle process exit
process.on('exit', (code, signal) => {
this.sendMessage(ws, {
type: 'end',
data: `Script execution finished with code: ${code}, signal: ${signal}`,
timestamp: Date.now()
});
// Clean up
this.activeExecutions.delete(executionId);
});
// Handle process error
process.on('error', (error) => {
this.sendMessage(ws, {
type: 'error',
data: `Process error: ${error.message}`,
timestamp: Date.now()
});
// Clean up
this.activeExecutions.delete(executionId);
});
} catch (error) {
this.sendMessage(ws, {
type: 'error',
data: `Failed to start script: ${error.message}`,
timestamp: Date.now()
});
}
}
stopScriptExecution(executionId) {
const execution = this.activeExecutions.get(executionId);
if (execution) {
execution.process.kill('SIGTERM');
this.activeExecutions.delete(executionId);
this.sendMessage(execution.ws, {
type: 'end',
data: 'Script execution stopped by user',
timestamp: Date.now()
});
}
}
sendMessage(ws, message) {
if (ws.readyState === 1) { // WebSocket.OPEN
ws.send(JSON.stringify(message));
}
}
cleanupActiveExecutions(ws) {
for (const [executionId, execution] of this.activeExecutions.entries()) {
if (execution.ws === ws) {
execution.process.kill('SIGTERM');
this.activeExecutions.delete(executionId);
}
}
}
}
app.prepare().then(() => {
const httpServer = createServer(async (req, res) => {
try {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;
if (pathname === '/ws/script-execution') {
// WebSocket upgrade will be handled by the WebSocket server
return;
}
await handle(req, res, parsedUrl);
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
});
// Create WebSocket handler
const scriptHandler = new ScriptExecutionHandler(httpServer);
httpServer
.once('error', (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`);
});
});