diff --git a/backend/agent-socket-handlers/terminal-socket-handler.ts b/backend/agent-socket-handlers/terminal-socket-handler.ts index 6367962..ab8fb5c 100644 --- a/backend/agent-socket-handlers/terminal-socket-handler.ts +++ b/backend/agent-socket-handlers/terminal-socket-handler.ts @@ -38,6 +38,11 @@ export class TerminalSocketHandler extends AgentSocketHandler { try { checkLogin(socket); + // Throw an error if console is not enabled + if (!server.config.enableConsole) { + throw new ValidationError("Console is not enabled."); + } + // TODO: Reset the name here, force one main terminal for now terminalName = "console"; @@ -66,6 +71,18 @@ export class TerminalSocketHandler extends AgentSocketHandler { } }); + // Check if MainTerminal is enabled + agentSocket.on("checkMainTerminal", async (callback) => { + try { + checkLogin(socket); + callbackResult({ + ok: server.config.enableConsole, + }, callback); + } catch (e) { + callbackError(e, callback); + } + }); + // Interactive Terminal for containers agentSocket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, shell : unknown, callback) => { try { diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 8f734cc..730b69f 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -136,6 +136,11 @@ export class DockgeServer { stacksDir: { type: String, optional: true, + }, + enableConsole: { + type: Boolean, + optional: true, + defaultValue: false, } }); @@ -149,6 +154,7 @@ export class DockgeServer { this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined; this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/"; this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir; + this.config.enableConsole = args.enableConsole || process.env.DOCKGE_ENABLE_CONSOLE === "true" || false; this.stacksDir = this.config.stacksDir; log.debug("server", this.config); diff --git a/backend/terminal.ts b/backend/terminal.ts index 4a5d6b2..36d26b3 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -4,7 +4,6 @@ import * as pty from "@homebridge/node-pty-prebuilt-multiarch"; import { LimitQueue } from "./utils/limit-queue"; import { DockgeSocket } from "./util-server"; import { - allowedCommandList, allowedRawKeys, PROGRESS_TERMINAL_ROWS, TERMINAL_COLS, TERMINAL_ROWS @@ -16,7 +15,6 @@ import { log } from "./log"; * Terminal for running commands, no user interaction */ export class Terminal { - protected static terminalMap : Map<string, Terminal> = new Map(); protected _ptyProcess? : pty.IPty; @@ -272,6 +270,11 @@ export class MainTerminal extends InteractiveTerminal { constructor(server : DockgeServer, name : string) { let shell; + // Throw an error if console is not enabled + if (!server.config.enableConsole) { + throw new Error("Console is not enabled."); + } + if (os.platform() === "win32") { if (commandExistsSync("pwsh.exe")) { shell = "pwsh.exe"; @@ -285,21 +288,6 @@ export class MainTerminal extends InteractiveTerminal { } public write(input : string) { - // For like Ctrl + C - if (allowedRawKeys.includes(input)) { - super.write(input); - return; - } - - // Check if the command is allowed - const cmdParts = input.split(" "); - const executable = cmdParts[0].trim(); - log.debug("console", "Executable: " + executable); - log.debug("console", "Executable length: " + executable.length); - - if (!allowedCommandList.includes(executable)) { - throw new Error("Command not allowed."); - } super.write(input); } } diff --git a/backend/util-server.ts b/backend/util-server.ts index 227ece0..de4aecd 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -30,6 +30,7 @@ export interface Arguments { hostname? : string; dataDir? : string; stacksDir? : string; + enableConsole? : boolean; } // Some config values are required diff --git a/common/util-common.ts b/common/util-common.ts index e067c28..cf141c9 100644 --- a/common/util-common.ts +++ b/common/util-common.ts @@ -107,17 +107,6 @@ export const COMBINED_TERMINAL_ROWS = 20; export const ERROR_TYPE_VALIDATION = 1; -export const allowedCommandList : string[] = [ - "docker", - "ls", - "cd", - "dir", -]; - -export const allowedRawKeys = [ - "\u0003", // Ctrl + C -]; - export const acceptedComposeFileNames = [ "compose.yaml", "docker-compose.yaml", diff --git a/frontend/src/pages/Console.vue b/frontend/src/pages/Console.vue index 7b566fc..d2b15e9 100644 --- a/frontend/src/pages/Console.vue +++ b/frontend/src/pages/Console.vue @@ -1,35 +1,36 @@ <template> <transition name="slide-fade" appear> - <div> + <div v-if="!processing"> <h1 class="mb-3">Console</h1> - <div> - <p> - {{ $t("Allowed commands:") }} - <template v-for="(command, index) in allowedCommandList" :key="command"> - <code>{{ command }}</code> + <Terminal v-if="enableConsole" class="terminal" :rows="20" mode="mainTerminal" name="console" :endpoint="endpoint"></Terminal> - <!-- No comma at the end --> - <span v-if="index !== allowedCommandList.length - 1">, </span> - </template> + <div v-else class="alert alert-warning shadow-box" role="alert"> + <h4 class="alert-heading">Console is not enabled</h4> + <p> + Console is a powerful tool that allows you to execute any commands such as <code>docker</code>, <code>rm</code> within the Dockge's container in this Web UI. + </p> + + <p> + It might be dangerous since this Dockge container is connecting to the host's Docker daemon. Also Dockge could be possibly taken down by commands like <code>rm -rf</code>. + </p> + + <p> + If you understand the risk, you can enable it by setting <code>DOCKGE_ENABLE_CONSOLE=true</code> in the environment variables. </p> </div> - - <Terminal class="terminal" :rows="20" mode="mainTerminal" name="console" :endpoint="endpoint"></Terminal> </div> </transition> </template> <script> - -import { allowedCommandList } from "../../../common/util-common"; - export default { components: { }, data() { return { - allowedCommandList, + processing: true, + enableConsole: false, }; }, computed: { @@ -38,7 +39,11 @@ export default { }, }, mounted() { - + console.log(this.endpoint); + this.$root.emitAgent(this.endpoint, "checkMainTerminal", (res) => { + this.enableConsole = res.ok; + this.processing = false; + }); }, methods: { diff --git a/package-lock.json b/package-lock.json index 09b93a3..506977c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "@types/semver": "^7.7.0", "@typescript-eslint/eslint-plugin": "~6.8.0", "@typescript-eslint/parser": "~6.8.0", - "@vitejs/plugin-vue": "~4.5.2", + "@vitejs/plugin-vue": "~5.2.3", "@xterm/addon-fit": "beta", "@xterm/xterm": "beta", "bootstrap": "5.3.2", @@ -2153,16 +2153,16 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz", - "integrity": "sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", "dev": true, "license": "MIT", "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^4.0.0 || ^5.0.0", + "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, diff --git a/package.json b/package.json index abc37a6..2803f53 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@types/semver": "^7.7.0", "@typescript-eslint/eslint-plugin": "~6.8.0", "@typescript-eslint/parser": "~6.8.0", - "@vitejs/plugin-vue": "~4.5.2", + "@vitejs/plugin-vue": "~5.2.3", "@xterm/addon-fit": "beta", "@xterm/xterm": "beta", "bootstrap": "5.3.2",