diff --git a/app.js b/app.js index b0d6c77..e1ccba2 100644 --- a/app.js +++ b/app.js @@ -380,14 +380,31 @@ function addConnection(topicHex) { connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between'; connectionItem.dataset.topicId = topicId; connectionItem.innerHTML = ` - - ${topicId} - - - `; + + ${topicId} + +
+ + +
+`; +// Add Docker Terminal button event listener +connectionItem.querySelector('.docker-terminal-btn').addEventListener('click', (e) => { + e.stopPropagation(); + const connection = connections[topicId]; + if (connection && connection.peer) { + import('./dockerTerminal.js').then(({ startDockerTerminal }) => { + startDockerTerminal(topicId, connection.peer); + }); + } else { + console.error('[ERROR] No active peer for Docker CLI terminal.'); + } +}) connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId)); connectionItem.querySelector('.disconnect-btn').addEventListener('click', (e) => { e.stopPropagation(); diff --git a/index.html b/index.html index 83ed5eb..5ae7e0e 100644 --- a/index.html +++ b/index.html @@ -515,6 +515,19 @@ + + +
diff --git a/libs/dockerTerminal.js b/libs/dockerTerminal.js new file mode 100644 index 0000000..31758da --- /dev/null +++ b/libs/dockerTerminal.js @@ -0,0 +1,100 @@ +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; + +// DOM Elements +const dockerTerminalModal = document.getElementById('docker-terminal-modal'); +const dockerTerminalTitle = document.getElementById('docker-terminal-title'); +const dockerTerminalContainer = document.getElementById('docker-terminal-container'); +const dockerKillTerminalBtn = document.getElementById('docker-kill-terminal-btn'); + +// Terminal variables +let dockerTerminalSession = null; + +// Start Docker CLI terminal session +function startDockerTerminal(connectionId, peer) { + if (!peer) { + console.error('[ERROR] No active peer for Docker CLI terminal.'); + return; + } + + if (dockerTerminalSession) { + console.log(`[INFO] Docker CLI terminal session already exists for connection: ${connectionId}`); + return; + } + + console.log(`[INFO] Starting Docker CLI terminal for connection: ${connectionId}`); + + const xterm = new Terminal({ + cursorBlink: true, + theme: { background: '#1a1a1a', foreground: '#ffffff' }, + }); + const fitAddon = new FitAddon(); + xterm.loadAddon(fitAddon); + + dockerTerminalContainer.innerHTML = ''; // Clear previous content + xterm.open(dockerTerminalContainer); + fitAddon.fit(); + + dockerTerminalSession = { xterm, fitAddon, connectionId, peer }; + + xterm.onData((input) => { + const sanitizedInput = sanitizeDockerCommand(input.trim()); + if (sanitizedInput) { + console.log(`[DEBUG] Sending Docker CLI command: ${sanitizedInput}`); + peer.write( + JSON.stringify({ + type: 'dockerCommand', + data: sanitizedInput, + }) + ); + } else { + xterm.write('\r\n[ERROR] Invalid command. Only Docker CLI commands are allowed.\r\n'); + } + }); + + peer.on('data', (data) => { + try { + const response = JSON.parse(data.toString()); + if (response.type === 'dockerOutput' && response.connectionId === connectionId) { + xterm.write(response.data); + } + } catch (error) { + console.error(`[ERROR] Failed to parse response from peer: ${error.message}`); + } + }); + + dockerTerminalTitle.textContent = `Docker CLI Terminal: ${connectionId}`; + dockerTerminalModal.style.display = 'flex'; +} + +// Sanitize input to ensure only Docker CLI commands are allowed +function sanitizeDockerCommand(command) { + const allowedCommands = ['docker', 'docker-compose']; + const parts = command.split(/\s+/); + const baseCommand = parts[0]; + + if (allowedCommands.includes(baseCommand)) { + return command; // Valid Docker command + } + + return null; // Invalid command +} + +// Clean up Docker CLI terminal session +function cleanUpDockerTerminal() { + if (dockerTerminalSession) { + dockerTerminalSession.xterm.dispose(); + dockerTerminalSession = null; + dockerTerminalContainer.innerHTML = ''; + dockerTerminalModal.style.display = 'none'; + console.log('[INFO] Docker CLI terminal session cleaned up.'); + } +} + +// Handle Kill Terminal button +dockerKillTerminalBtn.onclick = () => { + cleanUpDockerTerminal(); +}; + +// Export functions +export { startDockerTerminal, cleanUpDockerTerminal }; diff --git a/server/server.js b/server/server.js index 0f9aa3b..45377dd 100644 --- a/server/server.js +++ b/server/server.js @@ -55,7 +55,7 @@ swarm.on('connection', (peer) => { if (!(parsedData.command === 'stats' && Object.keys(parsedData.args).length === 0)) { console.log(`[DEBUG] Received data from peer: ${JSON.stringify(parsedData)}`); } - let response; + let response; switch (parsedData.command) { case 'listContainers': @@ -107,7 +107,44 @@ swarm.on('connection', (peer) => { await duplicateContainer(name, image, hostname, netmode, cpu, memoryInMB, dupConfig, peer); return; // Response is handled within the duplicateContainer function + case 'dockerCommand': + console.log(`[INFO] Executing Docker CLI command: ${parsedData.data}`); + try { + const exec = spawn('sh', ['-c', parsedData.data]); + exec.stdout.on('data', (output) => { + peer.write( + JSON.stringify({ + type: 'dockerOutput', + connectionId: parsedData.connectionId, + data: output.toString(), + }) + ); + }); + + exec.stderr.on('data', (error) => { + peer.write( + JSON.stringify({ + type: 'dockerOutput', + connectionId: parsedData.connectionId, + data: `[ERROR] ${error.toString()}`, + }) + ); + }); + + exec.on('close', (code) => { + peer.write( + JSON.stringify({ + type: 'dockerOutput', + connectionId: parsedData.connectionId, + data: `[INFO] Command exited with code ${code}\n`, + }) + ); + }); + } catch (error) { + peer.write(JSON.stringify({ error: `Failed to execute command: ${error.message}` })); + } + break; case 'startContainer': console.log(`[INFO] Handling 'startContainer' command for container: ${parsedData.args.id}`); await docker.getContainer(parsedData.args.id).start();