From 4f634530da4187db939e4b64cd18d52823fc6707 Mon Sep 17 00:00:00 2001 From: KodekaTeam Date: Thu, 4 Jun 2026 00:52:42 +0700 Subject: [PATCH] refactor: simplify error formatter in web-server modal --- .../ssh-services/docker-containers.ts | 5 +-- .../services/ssh-services/internal/shell.ts | 2 +- tests/backend/shell-escaping.test.ts | 43 +++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tests/backend/shell-escaping.test.ts diff --git a/src/server/services/ssh-services/docker-containers.ts b/src/server/services/ssh-services/docker-containers.ts index a3e657d..a1262c1 100644 --- a/src/server/services/ssh-services/docker-containers.ts +++ b/src/server/services/ssh-services/docker-containers.ts @@ -5,6 +5,7 @@ import { Server } from "@prisma/client"; import { execStrict } from "./commands"; import { execDocker, execDockerStrict } from "./internal/docker"; import { privilegedCommand } from "./internal/privilege"; +import { escapeShellArg } from "./internal/shell"; // NOTE: This file is a modularization of ssh.service.ts (domain: docker-containers). @@ -105,10 +106,6 @@ export async function dockerLogs( return result.stdout + result.stderr; } -function escapeShellArg(value: string): string { - return `'${value.replace(/'/g, `'"'"'`)}'`; -} - const SAFE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/; const SAFE_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; const SAFE_RESTART_POLICY_PATTERN = diff --git a/src/server/services/ssh-services/internal/shell.ts b/src/server/services/ssh-services/internal/shell.ts index 99639e6..149fa63 100644 --- a/src/server/services/ssh-services/internal/shell.ts +++ b/src/server/services/ssh-services/internal/shell.ts @@ -1,3 +1,3 @@ export function escapeShellArg(value: string): string { - return `'${value.replace(/'/g, `"'"'"`)}'`; + return `'${value.replace(/'/g, `'\\''`)}'`; } diff --git a/tests/backend/shell-escaping.test.ts b/tests/backend/shell-escaping.test.ts new file mode 100644 index 0000000..857fb4e --- /dev/null +++ b/tests/backend/shell-escaping.test.ts @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { escapeShellArg } from "../../src/server/services/ssh-services/internal/shell"; +import { formatServerActionError } from "../../src/server/routes/server-action-errors"; + +test("escapeShellArg preserves single quotes inside bash -lc payloads", () => { + const command = + "docker ps --format '{{.Names}}|{{.Image}}|{{.Ports}}' 2>/dev/null | grep -E '0\\.0\\.0\\.0:(80|443)|:::(80|443)' || true"; + + assert.equal( + escapeShellArg(command), + "'docker ps --format '\\''{{.Names}}|{{.Image}}|{{.Ports}}'\\'' 2>/dev/null | grep -E '\\''0\\.0\\.0\\.0:(80|443)|:::(80|443)'\\'' || true'", + ); +}); + +test("server action errors hide raw remote shell syntax failures", () => { + const message = formatServerActionError( + new Error( + "Command failed: bash: -c: line 1: syntax error near unexpected token `|'", + ), + "Failed to restart service apache2", + ); + + assert.equal( + message, + "The remote host rejected the generated shell command before it could finish. Refresh the server snapshot and retry the action.", + ); +}); + +test("server action errors normalize host port conflicts", () => { + const message = formatServerActionError( + new Error( + "Command failed: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)", + ), + "Failed to restart web server", + ); + + assert.equal( + message, + "The service could not start because port 80 or 443 is already in use by another host process or Docker container.", + ); +});