diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 4d594cb9e9..35c087db69 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -205,7 +205,42 @@ set_env_var() { fi } +# Quote a value so that Docker Compose and phpdotenv both read it back unchanged (same as install.sh). +quote_env_value() { + local value="$1" + case "$value" in + *\'*) + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//\$/\\\$} + printf '"%s"' "$value" + ;; + *) + printf "'%s'" "$value" + ;; + esac +} + +# Installers before 4.5.1 (and manual edits) wrote root user credentials with spaces without quotes. +# phpdotenv cannot parse such a line, so the recreated Coolify container returns HTTP 500 for every page. +# Only quote lines that phpdotenv rejects: unquoted values that contain whitespace but no comment. +quote_root_user_env_values() { + local key value + for key in ROOT_USERNAME ROOT_USER_EMAIL ROOT_USER_PASSWORD; do + value=$(grep -m1 "^${key}=" "$ENV_FILE" | cut -d '=' -f2-) || continue + case "$value" in + \"* | \'* | *[[:space:]]\#*) continue ;; + *[[:space:]]*) ;; + *) continue ;; + esac + # awk reads the value from the environment, so characters such as | & \ in it stay literal + KEY="$key" VALUE="$(quote_env_value "$value")" awk '!done && index($0, ENVIRON["KEY"] "=") == 1 { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"]; done = 1 } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" + log "Quoted ${key} (its value has spaces)" + done +} + log "Checking environment variables..." +quote_root_user_env_values set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 1e1e23cadd..9377d79c5a 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -205,7 +205,42 @@ set_env_var() { fi } +# Quote a value so that Docker Compose and phpdotenv both read it back unchanged (same as install.sh). +quote_env_value() { + local value="$1" + case "$value" in + *\'*) + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//\$/\\\$} + printf '"%s"' "$value" + ;; + *) + printf "'%s'" "$value" + ;; + esac +} + +# Installers before 4.5.1 (and manual edits) wrote root user credentials with spaces without quotes. +# phpdotenv cannot parse such a line, so the recreated Coolify container returns HTTP 500 for every page. +# Only quote lines that phpdotenv rejects: unquoted values that contain whitespace but no comment. +quote_root_user_env_values() { + local key value + for key in ROOT_USERNAME ROOT_USER_EMAIL ROOT_USER_PASSWORD; do + value=$(grep -m1 "^${key}=" "$ENV_FILE" | cut -d '=' -f2-) || continue + case "$value" in + \"* | \'* | *[[:space:]]\#*) continue ;; + *[[:space:]]*) ;; + *) continue ;; + esac + # awk reads the value from the environment, so characters such as | & \ in it stay literal + KEY="$key" VALUE="$(quote_env_value "$value")" awk '!done && index($0, ENVIRON["KEY"] "=") == 1 { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"]; done = 1 } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" + log "Quoted ${key} (its value has spaces)" + done +} + log "Checking environment variables..." +quote_root_user_env_values set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" diff --git a/tests/Unit/UpgradeScriptRootUserEnvTest.php b/tests/Unit/UpgradeScriptRootUserEnvTest.php new file mode 100644 index 0000000000..e77ea3f6a2 --- /dev/null +++ b/tests/Unit/UpgradeScriptRootUserEnvTest.php @@ -0,0 +1,67 @@ +not->toBeEmpty() + ->and($quoteRootUserEnvValues)->not->toBeEmpty(); + + $envFile = tempnam(sys_get_temp_dir(), 'coolify-upgrade-env-'); + file_put_contents($envFile, $initialEnv); + + $process = proc_open( + ['bash', '-c', implode("\n", ['set -e', 'log() { :; }', 'ENV_FILE='.escapeshellarg($envFile), $quoteEnvValue[0], $quoteRootUserEnvValues[0], 'quote_root_user_env_values'])], + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes + ); + $output = stream_get_contents($pipes[1]).stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + expect(proc_close($process))->toBe(0, $output); + + $contents = file_get_contents($envFile); + unlink($envFile); + + return $contents; +} + +$upgradeScripts = ['stable upgrade' => 'scripts/upgrade.sh', 'nightly upgrade' => 'other/nightly/upgrade.sh']; + +it('quotes unquoted root user credentials with spaces so that .env parses again', function (string $scriptPath) { + $env = repairRootUserEnvWithUpgradeScript($scriptPath, implode("\n", [ + 'APP_NAME=Coolify', + 'ROOT_USERNAME=Jane Doe', + 'ROOT_USER_EMAIL=admin@example.com', + "ROOT_USER_PASSWORD=it's a P@ss|&\\ \$HOME", + 'APP_ENV=production', + '', + ])); + + expect(Dotenv::parse($env))->toBe([ + 'APP_NAME' => 'Coolify', + 'ROOT_USERNAME' => 'Jane Doe', + 'ROOT_USER_EMAIL' => 'admin@example.com', + 'ROOT_USER_PASSWORD' => "it's a P@ss|&\\ \$HOME", + 'APP_ENV' => 'production', + ]); +})->with($upgradeScripts); + +it('keeps root user credentials that already parse unchanged', function (string $scriptPath) { + $initialEnv = implode("\n", [ + 'ROOT_USERNAME="Jane Doe"', + "ROOT_USER_EMAIL='admin@example.com'", + 'ROOT_USER_PASSWORD=', + 'APP_NAME=My Coolify', + '', + ]); + + expect(repairRootUserEnvWithUpgradeScript($scriptPath, $initialEnv))->toBe($initialEnv); +})->with($upgradeScripts);