add docker cli access per connection

This commit is contained in:
Raven Scott 2024-12-01 23:48:26 -05:00
parent 9389d41364
commit 3e37359e61
4 changed files with 257 additions and 107 deletions

58
app.js
View File

@ -1,6 +1,7 @@
import Hyperswarm from 'hyperswarm';
import b4a from 'b4a';
import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
// DOM Elements
const containerList = document.getElementById('container-list');
@ -32,6 +33,16 @@ function stopStatsInterval() {
}
}
document.addEventListener('DOMContentLoaded', () => {
const dockerTerminalModal = document.getElementById('dockerTerminalModal');
if (dockerTerminalModal) {
dockerTerminalModal.addEventListener('hidden.bs.modal', () => {
console.log('[INFO] Modal fully closed. Performing additional cleanup.');
cleanUpDockerTerminal();
});
}
});
function startStatsInterval() {
if (statsInterval) {
@ -304,8 +315,6 @@ function handlePeerData(data, topicId, peer) {
switch (response.type) {
case 'stats':
console.log('[INFO] Updating container stats...');
// Ensure IP is included and passed to updateContainerStats
const stats = response.data;
stats.ip = stats.ip || 'No IP Assigned'; // Add a fallback for missing IPs
console.log(`[DEBUG] Passing stats to updateContainerStats: ${JSON.stringify(stats, null, 2)}`);
@ -352,6 +361,7 @@ function handlePeerData(data, topicId, peer) {
// Add a new connection
addConnectionForm.addEventListener('submit', (e) => {
e.preventDefault();
@ -394,17 +404,41 @@ function addConnection(topicHex) {
`;
// 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('./libs/dockerTerminal.js').then(({ startDockerTerminal }) => {
startDockerTerminal(topicId, connection.peer);
});
} else {
console.error('[ERROR] No active peer for Docker CLI terminal.');
connectionItem.querySelector('.docker-terminal-btn')?.addEventListener('click', (event) => {
event.stopPropagation();
console.log('[DEBUG] Docker terminal button clicked.');
if (!topicId) {
console.error('[ERROR] Missing topicId. Cannot proceed.');
return;
}
})
const connection = connections[topicId];
console.log(`[DEBUG] Retrieved connection for topicId: ${topicId}`, connection);
if (connection && connection.peer) {
try {
console.log(`[DEBUG] Starting Docker terminal for topicId: ${topicId}`);
startDockerTerminal(topicId, connection.peer);
const dockerTerminalModal = document.getElementById('dockerTerminalModal');
if (dockerTerminalModal) {
const modalInstance = new bootstrap.Modal(dockerTerminalModal);
modalInstance.show();
console.log('[DEBUG] Docker Terminal modal displayed.');
} else {
console.error('[ERROR] Docker Terminal modal not found in the DOM.');
}
} catch (error) {
console.error(`[ERROR] Failed to start Docker CLI terminal for topicId: ${topicId}`, error);
}
} else {
console.warn(`[WARNING] No active peer found for topicId: ${topicId}. Unable to start Docker CLI terminal.`);
}
});
connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId));
connectionItem.querySelector('.disconnect-btn').addEventListener('click', (e) => {
e.stopPropagation();

View File

@ -375,6 +375,7 @@
background-color: #999;
/* Even lighter color when active */
}
</style>
</head>
@ -515,17 +516,19 @@
</div>
</div>
<!-- Docker Terminal Modal -->
<!-- Docker CLI Terminal Modal -->
<div id="docker-terminal-modal" class="position-fixed bottom-0 start-0 w-100 max-height-90 bg-dark text-white d-flex flex-column" style="display: none; z-index: 1000;">
<div class="header d-flex justify-content-between align-items-center bg-secondary p-2">
<span id="docker-terminal-title" class="fw-bold">Docker CLI Terminal</span>
<button id="docker-kill-terminal-btn" class="btn btn-sm btn-danger">
<i class="fas fa-times-circle"></i>
</button>
<!-- Docker Terminal Modal -->
<div class="modal fade" id="dockerTerminalModal" tabindex="-1" aria-labelledby="docker-terminal-title" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 id="docker-terminal-title" class="modal-title">Docker CLI Terminal</h5>
<button id="docker-kill-terminal-btn" type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="docker-terminal-container"></div>
</div>
</div>
</div>
<div id="docker-terminal-container" class="flex-grow-1 overflow-hidden" style="background-color: black;"></div>
<div id="docker-terminal-resize-handle" class="bg-secondary" style="height: 10px; cursor: ns-resize;"></div>
</div>
<!-- Alert Container -->

View File

@ -10,6 +10,11 @@ const dockerKillTerminalBtn = document.getElementById('docker-kill-terminal-btn'
// Terminal variables
let dockerTerminalSession = null;
/**
* Initialize and start the Docker CLI terminal.
* @param {string} connectionId - Unique ID for the connection.
* @param {Object} peer - Active peer object for communication.
*/
function startDockerTerminal(connectionId, peer) {
if (!peer) {
console.error('[ERROR] No active peer for Docker CLI terminal.');
@ -21,6 +26,18 @@ function startDockerTerminal(connectionId, peer) {
return;
}
// Verify DOM elements
const dockerTerminalContainer = document.getElementById('docker-terminal-container');
const dockerTerminalTitle = document.getElementById('docker-terminal-title');
const dockerTerminalModal = document.getElementById('dockerTerminalModal');
const dockerKillTerminalBtn = document.getElementById('docker-kill-terminal-btn');
if (!dockerTerminalContainer || !dockerTerminalTitle || !dockerTerminalModal || !dockerKillTerminalBtn) {
console.error('[ERROR] Missing required DOM elements for Docker CLI terminal.');
return;
}
// Initialize the xterm.js terminal
const xterm = new Terminal({
cursorBlink: true,
theme: { background: '#000000', foreground: '#ffffff' },
@ -28,84 +45,168 @@ function startDockerTerminal(connectionId, peer) {
const fitAddon = new FitAddon();
xterm.loadAddon(fitAddon);
// Prepare the terminal container
dockerTerminalContainer.innerHTML = ''; // Clear previous content
xterm.open(dockerTerminalContainer);
fitAddon.fit();
dockerTerminalSession = { xterm, fitAddon, connectionId, peer };
let inputBuffer = ''; // Buffer for user input
// Buffer to accumulate user input
let inputBuffer = '';
// Handle terminal input
xterm.onData((input) => {
if (input === '\r') { // User pressed Enter
const sanitizedInput = sanitizeDockerCommand(inputBuffer.trim());
if (sanitizedInput) {
console.log(`[DEBUG] Sending Docker CLI command: ${sanitizedInput}`);
if (input === '\r') {
// User pressed Enter
const fullCommand = prependDockerCommand(inputBuffer.trim());
if (fullCommand) {
console.log(`[DEBUG] Sending Docker CLI command: ${fullCommand}`);
peer.write(
JSON.stringify({
type: 'dockerCommand',
command: 'dockerCommand',
connectionId,
data: sanitizedInput,
data: fullCommand,
})
);
xterm.write('\r\n'); // Move to the next line in the terminal
xterm.write('\r\n'); // Move to the next line
} else {
xterm.write('\r\n[ERROR] Invalid command. Only Docker CLI commands are allowed.\r\n');
xterm.write('\r\n[ERROR] Invalid command. Please check your input.\r\n');
}
inputBuffer = ''; // Clear the buffer after processing
} else if (input === '\u007F') { // Handle backspace
} else if (input === '\u007F') {
// Handle backspace
if (inputBuffer.length > 0) {
inputBuffer = inputBuffer.slice(0, -1); // Remove the last character from the buffer
xterm.write('\b \b'); // Erase the character from the terminal display
inputBuffer = inputBuffer.slice(0, -1); // Remove last character
xterm.write('\b \b'); // Erase character from display
}
} else {
inputBuffer += input; // Append input to the buffer
xterm.write(input); // Display input in the terminal
// Append input to buffer and display it
inputBuffer += input;
xterm.write(input);
}
});
// Handle peer data
peer.on('data', (data) => {
console.log('[DEBUG] Received data event');
try {
const response = JSON.parse(data.toString());
if (response.type === 'dockerOutput' && response.connectionId === connectionId) {
xterm.write(`${response.data}\r\n`);
if (response.connectionId === connectionId) {
const decodedData = decodeResponseData(response.data, response.encoding);
if (response.type === 'dockerOutput') {
xterm.write(`${decodedData.trim()}\r\n`);
} else if (response.type === 'terminalErrorOutput') {
xterm.write(`\r\n[ERROR] ${decodedData.trim()}\r\n`);
}
}
} catch (error) {
console.error(`[ERROR] Failed to parse response from peer: ${error.message}`);
}
});
// Update the terminal modal and title
dockerTerminalTitle.textContent = `Docker CLI Terminal: ${connectionId}`;
dockerTerminalModal.style.display = 'flex';
const modalInstance = new bootstrap.Modal(dockerTerminalModal);
modalInstance.show();
// Attach event listener for Kill Terminal button
dockerKillTerminalBtn.onclick = () => {
cleanUpDockerTerminal();
};
}
// Sanitize input to ensure only Docker CLI commands are allowed
function sanitizeDockerCommand(command) {
// Allow commands starting with "docker" and disallow dangerous operators
const isValid = /^docker(\s+[\w.-]+)*$/i.test(command);
return isValid ? command : null;
/**
* Prepend 'docker' to the command if it's missing.
* @param {string} command - Command string entered by the user.
* @returns {string|null} - Full Docker command or null if invalid.
*/
function prependDockerCommand(command) {
// Disallow dangerous operators
if (/[\|;&`]/.test(command)) {
console.warn('[WARN] Invalid characters detected in command.');
return null;
}
// Clean up Docker CLI terminal session
// Prepend 'docker' if not already present
if (!command.startsWith('docker ')) {
return `docker ${command}`;
}
return command;
}
/**
* Decode response data from Base64 or return as-is if not encoded.
* @param {string} data - Response data from the server.
* @param {string} encoding - Encoding type (e.g., 'base64').
* @returns {string} - Decoded or plain data.
*/
function decodeResponseData(data, encoding) {
if (encoding === 'base64') {
try {
return atob(data); // Decode Base64 data
} catch (error) {
console.error(`[ERROR] Failed to decode Base64 data: ${error.message}`);
return '[ERROR] Command failed.';
}
}
return data; // Return plain data if not encoded
}
/**
* Clean up the Docker CLI terminal session.
*/
function cleanUpDockerTerminal() {
if (dockerTerminalSession) {
dockerTerminalSession.xterm.dispose();
dockerTerminalSession = null;
dockerTerminalContainer.innerHTML = ''; // Clear terminal content
dockerTerminalModal.style.display = 'none'; // Hide the modal
dockerTerminalModal.classList.add('hidden');
console.log('[INFO] Docker CLI terminal session cleaned up.');
}
console.log('[INFO] Cleaning up Docker Terminal...');
// Retrieve the required DOM elements
const dockerTerminalContainer = document.getElementById('docker-terminal-container');
const dockerTerminalModal = document.getElementById('dockerTerminalModal');
if (!dockerTerminalContainer || !dockerTerminalModal) {
console.error('[ERROR] Required DOM elements not found for cleaning up the Docker Terminal.');
return;
}
// Handle Kill Terminal button
// Dispose of the terminal session if it exists
if (dockerTerminalSession) {
if (dockerTerminalSession.xterm) {
dockerTerminalSession.xterm.dispose();
}
dockerTerminalSession = null; // Reset the session object
}
// Clear the terminal content
dockerTerminalContainer.innerHTML = '';
// Use Bootstrap API to hide the modal
const modalInstance = bootstrap.Modal.getInstance(dockerTerminalModal);
if (modalInstance) {
modalInstance.hide();
} else {
console.warn('[WARNING] Modal instance not found. Falling back to manual close.');
dockerTerminalModal.style.display = 'none';
}
// Ensure lingering backdrops are removed
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.remove();
}
// Restore the body's scroll behavior
document.body.classList.remove('modal-open');
document.body.style.paddingRight = '';
console.log('[INFO] Docker CLI terminal session cleanup completed.');
}
// Attach event listener for Kill Terminal button (redundant safety check)
dockerKillTerminalBtn.onclick = () => {
cleanUpDockerTerminal();
};
// Export functions
export { startDockerTerminal, cleanUpDockerTerminal };
export { startDockerTerminal, cleanUpDockerTerminal, dockerTerminalSession };

View File

@ -7,6 +7,7 @@ import { PassThrough } from 'stream';
import os from "os";
import fs from 'fs';
import dotenv from 'dotenv';
import { spawn } from 'child_process';
// Load environment variables from .env file
dotenv.config();
@ -98,57 +99,59 @@ swarm.on('connection', (peer) => {
const config = await container.inspect();
response = { type: 'containerConfig', data: config };
break;
case 'duplicateContainer':
console.log('[INFO] Handling \'duplicateContainer\' command');
const { name, image, hostname, netmode, cpu, memory, config: dupConfig } = parsedData.args;
const memoryInMB = memory * 1024 * 1024;
console.log("MEMEMMEMEMEMEMEMEMMEME " + memoryInMB)
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}`);
console.log(`[INFO] Handling 'dockerCommand' with data: ${parsedData.data}`);
try {
const exec = spawn('sh', ['-c', parsedData.data]); // Use spawn to run the Docker command
const command = parsedData.data.split(' '); // Split the command into executable and args
const executable = command[0];
const args = command.slice(1);
// Handle stdout (command output)
exec.stdout.on('data', (output) => {
console.log(`[DEBUG] Command output: ${output.toString()}`);
peer.write(
JSON.stringify({
const childProcess = spawn(executable, args);
let response = {
type: 'dockerOutput',
connectionId: parsedData.connectionId,
data: output.toString(),
data: '',
};
// Stream stdout to the peer
childProcess.stdout.on('data', (data) => {
console.log(`[DEBUG] Command stdout: ${data.toString()}`);
peer.write(
JSON.stringify({
...response,
data: data.toString('base64'),
encoding: 'base64',
})
);
});
// Handle stderr (errors)
exec.stderr.on('data', (error) => {
console.error(`[ERROR] Command error output: ${error.toString()}`);
// Stream stderr to the peer
childProcess.stderr.on('data', (data) => {
console.error(`[ERROR] Command stderr: ${data.toString()}`);
peer.write(
JSON.stringify({
type: 'dockerOutput',
connectionId: parsedData.connectionId,
data: `[ERROR] ${error.toString()}`,
...response,
data: `[ERROR] ${data.toString('base64')}`,
encoding: 'base64',
})
);
});
// Handle command completion
exec.on('close', (code) => {
console.log(`[INFO] Command exited with code: ${code}`);
// Handle command exit
childProcess.on('close', (code) => {
const exitMessage = `[INFO] Command exited with code ${code}`;
console.log(exitMessage);
peer.write(
JSON.stringify({
type: 'dockerOutput',
connectionId: parsedData.connectionId,
data: `[INFO] Command exited with code ${code}\n`,
...response,
data: exitMessage,
})
);
});
} catch (error) {
console.error(`[ERROR] Failed to execute command: ${error.message}`);
console.error(`[ERROR] Command execution failed: ${error.message}`);
peer.write(
JSON.stringify({
type: 'dockerOutput',
@ -158,6 +161,15 @@ swarm.on('connection', (peer) => {
);
}
break;
case 'duplicateContainer':
console.log('[INFO] Handling \'duplicateContainer\' command');
const { name, image, hostname, netmode, cpu, memory, config: dupConfig } = parsedData.args;
const memoryInMB = memory * 1024 * 1024;
console.log("MEMEMMEMEMEMEMEMEMMEME " + memoryInMB)
await duplicateContainer(name, image, hostname, netmode, cpu, memoryInMB, dupConfig, peer);
return; // Response is handled within the duplicateContainer function
case 'startContainer':
console.log(`[INFO] Handling 'startContainer' command for container: ${parsedData.args.id}`);
await docker.getContainer(parsedData.args.id).start();