From 9bb77da9a429c8f398fec31f384a17fc4d1a4357 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:02:00 +0100 Subject: [PATCH] Add Arch Linux server support and fix package sanitization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Arch Linux (pacman) support to server operations: CheckUpdates, InstallDocker, InstallPrerequisites, UpdatePackage - Implement parsePacmanOutput() to parse 'pacman -Qu' output format - Add security improvement: package name sanitization to prevent command injection - Initialize variables in CheckUpdates to prevent undefined variable errors in catch block - Use proper Arch pacman flags: -Syu for full system upgrade before operations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Actions/Server/CheckUpdates.php | 54 +++++++++++++++++++++ app/Actions/Server/InstallDocker.php | 12 +++++ app/Actions/Server/InstallPrerequisites.php | 7 +++ app/Actions/Server/UpdatePackage.php | 24 +++++++-- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/app/Actions/Server/CheckUpdates.php b/app/Actions/Server/CheckUpdates.php index 6823dfb92..f90e00708 100644 --- a/app/Actions/Server/CheckUpdates.php +++ b/app/Actions/Server/CheckUpdates.php @@ -13,6 +13,9 @@ class CheckUpdates public function handle(Server $server) { + $osId = 'unknown'; + $packageManager = null; + try { if ($server->serverStatus() === false) { return [ @@ -93,6 +96,16 @@ public function handle(Server $server) $out['osId'] = $osId; $out['package_manager'] = $packageManager; + return $out; + case 'pacman': + // Sync database first, then check for updates + // Using -Sy to refresh package database before querying available updates + instant_remote_process(['pacman -Sy'], $server); + $output = instant_remote_process(['pacman -Qu 2>/dev/null'], $server); + $out = $this->parsePacmanOutput($output); + $out['osId'] = $osId; + $out['package_manager'] = $packageManager; + return $out; default: return [ @@ -219,4 +232,45 @@ private function parseAptOutput(string $output): array 'updates' => $updates, ]; } + + private function parsePacmanOutput(string $output): array + { + $updates = []; + $unparsedLines = []; + $lines = explode("\n", $output); + + foreach ($lines as $line) { + if (empty($line)) { + continue; + } + // Format: package current_version -> new_version + if (preg_match('/^(\S+)\s+(\S+)\s+->\s+(\S+)$/', $line, $matches)) { + $updates[] = [ + 'package' => $matches[1], + 'current_version' => $matches[2], + 'new_version' => $matches[3], + 'architecture' => 'unknown', + 'repository' => 'unknown', + ]; + } else { + // Log unmatched lines for debugging purposes + $unparsedLines[] = $line; + } + } + + $result = [ + 'total_updates' => count($updates), + 'updates' => $updates, + ]; + + // Include unparsed lines in the result for debugging if any exist + if (! empty($unparsedLines)) { + $result['unparsed_lines'] = $unparsedLines; + \Illuminate\Support\Facades\Log::debug('Pacman output contained unparsed lines', [ + 'unparsed_lines' => $unparsedLines, + ]); + } + + return $result; + } } diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 36c540950..55a643e83 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -78,6 +78,8 @@ public function handle(Server $server) $command = $command->merge([$this->getRhelDockerInstallCommand()]); } elseif ($supported_os_type->contains('sles')) { $command = $command->merge([$this->getSuseDockerInstallCommand()]); + } elseif ($supported_os_type->contains('arch')) { + $command = $command->merge([$this->getArchDockerInstallCommand()]); } else { $command = $command->merge([$this->getGenericDockerInstallCommand()]); } @@ -150,4 +152,14 @@ private function getGenericDockerInstallCommand(): string { return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion}"; } + + private function getArchDockerInstallCommand(): string + { + // Use -Syu to perform full system upgrade before installing Docker + // Partial upgrades (-Sy without -u) are discouraged on Arch Linux + // as they can lead to broken dependencies and system instability + return 'pacman -Syu --noconfirm docker docker-compose && '. + 'systemctl enable docker.service && '. + 'systemctl start docker.service'; + } } diff --git a/app/Actions/Server/InstallPrerequisites.php b/app/Actions/Server/InstallPrerequisites.php index 1a7d3bbd9..84be7f206 100644 --- a/app/Actions/Server/InstallPrerequisites.php +++ b/app/Actions/Server/InstallPrerequisites.php @@ -46,6 +46,13 @@ public function handle(Server $server) 'command -v git >/dev/null || zypper install -y git', 'command -v jq >/dev/null || zypper install -y jq', ]); + } elseif ($supported_os_type->contains('arch')) { + // Use -Syu for full system upgrade to avoid partial upgrade issues on Arch Linux + // --needed flag skips packages that are already installed and up-to-date + $command = $command->merge([ + "echo 'Installing Prerequisites for Arch Linux...'", + 'pacman -Syu --noconfirm --needed curl wget git jq', + ]); } else { throw new \Exception('Unsupported OS type for prerequisites installation'); } diff --git a/app/Actions/Server/UpdatePackage.php b/app/Actions/Server/UpdatePackage.php index 75d931f93..8b81377ee 100644 --- a/app/Actions/Server/UpdatePackage.php +++ b/app/Actions/Server/UpdatePackage.php @@ -20,18 +20,36 @@ public function handle(Server $server, string $osId, ?string $package = null, ?s 'error' => 'Server is not reachable or not ready.', ]; } + + // Sanitize package name to prevent command injection + // Only allow alphanumeric characters, hyphens, underscores, periods, plus signs, and colons + // These are valid characters in package names across most package managers + $sanitizedPackage = ''; + if ($package !== null && ! $all) { + if (! preg_match('/^[a-zA-Z0-9._+:-]+$/', $package)) { + return [ + 'error' => 'Invalid package name. Package names can only contain alphanumeric characters, hyphens, underscores, periods, plus signs, and colons.', + ]; + } + $sanitizedPackage = escapeshellarg($package); + } + switch ($packageManager) { case 'zypper': $commandAll = 'zypper update -y'; - $commandInstall = 'zypper install -y '.$package; + $commandInstall = 'zypper install -y '.$sanitizedPackage; break; case 'dnf': $commandAll = 'dnf update -y'; - $commandInstall = 'dnf update -y '.$package; + $commandInstall = 'dnf update -y '.$sanitizedPackage; break; case 'apt': $commandAll = 'apt update && apt upgrade -y'; - $commandInstall = 'apt install -y '.$package; + $commandInstall = 'apt install -y '.$sanitizedPackage; + break; + case 'pacman': + $commandAll = 'pacman -Syu --noconfirm'; + $commandInstall = 'pacman -S --noconfirm '.$sanitizedPackage; break; default: return [