add docker cli access per connection
This commit is contained in:
parent
9389d41364
commit
3e37359e61
58
app.js
58
app.js
@ -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();
|
||||
|
23
index.html
23
index.html
@ -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 -->
|
||||
|
@ -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
|
||||
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.');
|
||||
// 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
|
||||
}
|
||||
|
||||
// Handle Kill Terminal button
|
||||
/**
|
||||
* Clean up the Docker CLI terminal session.
|
||||
*/
|
||||
function cleanUpDockerTerminal() {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 };
|
||||
|
114
server/server.js
114
server/server.js
@ -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,6 +99,68 @@ swarm.on('connection', (peer) => {
|
||||
const config = await container.inspect();
|
||||
response = { type: 'containerConfig', data: config };
|
||||
break;
|
||||
case 'dockerCommand':
|
||||
console.log(`[INFO] Handling 'dockerCommand' with data: ${parsedData.data}`);
|
||||
|
||||
try {
|
||||
const command = parsedData.data.split(' '); // Split the command into executable and args
|
||||
const executable = command[0];
|
||||
const args = command.slice(1);
|
||||
|
||||
const childProcess = spawn(executable, args);
|
||||
|
||||
let response = {
|
||||
type: 'dockerOutput',
|
||||
connectionId: parsedData.connectionId,
|
||||
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',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Stream stderr to the peer
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
console.error(`[ERROR] Command stderr: ${data.toString()}`);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
...response,
|
||||
data: `[ERROR] ${data.toString('base64')}`,
|
||||
encoding: 'base64',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle command exit
|
||||
childProcess.on('close', (code) => {
|
||||
const exitMessage = `[INFO] Command exited with code ${code}`;
|
||||
console.log(exitMessage);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
...response,
|
||||
data: exitMessage,
|
||||
})
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Command execution failed: ${error.message}`);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
type: 'dockerOutput',
|
||||
connectionId: parsedData.connectionId,
|
||||
data: `[ERROR] Failed to execute command: ${error.message}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'duplicateContainer':
|
||||
console.log('[INFO] Handling \'duplicateContainer\' command');
|
||||
@ -107,57 +170,6 @@ 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]); // Use spawn to run the Docker command
|
||||
|
||||
// Handle stdout (command output)
|
||||
exec.stdout.on('data', (output) => {
|
||||
console.log(`[DEBUG] Command output: ${output.toString()}`);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
type: 'dockerOutput',
|
||||
connectionId: parsedData.connectionId,
|
||||
data: output.toString(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle stderr (errors)
|
||||
exec.stderr.on('data', (error) => {
|
||||
console.error(`[ERROR] Command error output: ${error.toString()}`);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
type: 'dockerOutput',
|
||||
connectionId: parsedData.connectionId,
|
||||
data: `[ERROR] ${error.toString()}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle command completion
|
||||
exec.on('close', (code) => {
|
||||
console.log(`[INFO] Command exited with code: ${code}`);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
type: 'dockerOutput',
|
||||
connectionId: parsedData.connectionId,
|
||||
data: `[INFO] Command exited with code ${code}\n`,
|
||||
})
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Failed to execute command: ${error.message}`);
|
||||
peer.write(
|
||||
JSON.stringify({
|
||||
type: 'dockerOutput',
|
||||
connectionId: parsedData.connectionId,
|
||||
data: `[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();
|
||||
|
Loading…
Reference in New Issue
Block a user