diff --git a/install.ps1 b/install.ps1 index 88db452..f3afc82 100644 --- a/install.ps1 +++ b/install.ps1 @@ -255,7 +255,14 @@ function Write-Utf8File { $enc = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($Path, $Content, $enc) } - + +# Single-quote a value for .env/.conf files so special characters ($, &, ;, etc.) +# are preserved. Embedded single quotes are escaped as '\''. +function Format-EnvVal([string]$Key, [string]$Val) { + $escaped = $Val -replace "'", "'\\''" + return "$Key='$escaped'" +} + # --- Config file --- function Load-ConfigFile { @@ -266,6 +273,12 @@ function Load-ConfigFile { if ($line -match '^\s*([^=]+?)\s*=\s*(.*?)\s*$') { $key = $Matches[1].Trim() $val = $Matches[2].Trim() + # Strip surrounding quotes and unescape '\'' + if ($val.Length -ge 2 -and $val[0] -eq "'" -and $val[-1] -eq "'") { + $val = $val.Substring(1, $val.Length - 2) -replace "'\\''" , "'" + } elseif ($val.Length -ge 2 -and $val[0] -eq '"' -and $val[-1] -eq '"') { + $val = $val.Substring(1, $val.Length - 2) + } switch ($key) { 'install_dir' { if (-not $script:cfg.InstallDir) { $script:cfg.InstallDir = $val } } 'public_host' { if (-not $script:cfg.PublicHost) { $script:cfg.PublicHost = $val } } @@ -669,15 +682,10 @@ HTTPS_PORT=$($c.HttpsPort) # PostgreSQL POSTGRES_USER=cameleer -POSTGRES_PASSWORD=$($c.PostgresPassword) POSTGRES_DB=cameleer -# ClickHouse -CLICKHOUSE_PASSWORD=$($c.ClickhousePassword) - # Server admin SERVER_ADMIN_USER=$($c.AdminUser) -SERVER_ADMIN_PASS=$($c.AdminPass) # Bootstrap token BOOTSTRAP_TOKEN=$bt @@ -697,6 +705,10 @@ CLICKHOUSE_IMAGE=$($c.Registry)/cameleer-clickhouse SERVER_IMAGE=$($c.Registry)/cameleer-server SERVER_UI_IMAGE=$($c.Registry)/cameleer-server-ui "@ + # Passwords appended with single-quoting for special character safety + $content += "`n$(Format-EnvVal 'POSTGRES_PASSWORD' $c.PostgresPassword)" + $content += "`n$(Format-EnvVal 'CLICKHOUSE_PASSWORD' $c.ClickhousePassword)" + $content += "`n$(Format-EnvVal 'SERVER_ADMIN_PASS' $c.AdminPass)" if ($c.TlsMode -eq 'custom') { $content += "`nCERT_FILE=/user-certs/cert.pem" $content += "`nKEY_FILE=/user-certs/key.pem" @@ -728,16 +740,11 @@ LOGTO_CONSOLE_BIND=$consoleBind # PostgreSQL POSTGRES_USER=cameleer -POSTGRES_PASSWORD=$($c.PostgresPassword) POSTGRES_DB=cameleer_saas - -# ClickHouse -CLICKHOUSE_PASSWORD=$($c.ClickhousePassword) - + # Admin user SAAS_ADMIN_USER=$($c.AdminUser) -SAAS_ADMIN_PASS=$($c.AdminPass) - + # TLS NODE_TLS_REJECT=$($c.NodeTlsReject) "@ @@ -772,10 +779,14 @@ CAMELEER_SERVER_SECURITY_JWTSECRET=$jwtSecret SMTP_HOST=$($c.SmtpHost) SMTP_PORT=$(if ($c.SmtpPort) { $c.SmtpPort } else { '587' }) SMTP_USER=$($c.SmtpUser) -SMTP_PASS=$($c.SmtpPass) SMTP_FROM_EMAIL=$(if ($c.SmtpFromEmail) { $c.SmtpFromEmail } else { "noreply@$($c.PublicHost)" }) "@ $content += $provisioningBlock + # Passwords appended with single-quoting for special character safety + $content += "`n$(Format-EnvVal 'POSTGRES_PASSWORD' $c.PostgresPassword)" + $content += "`n$(Format-EnvVal 'CLICKHOUSE_PASSWORD' $c.ClickhousePassword)" + $content += "`n$(Format-EnvVal 'SAAS_ADMIN_PASS' $c.AdminPass)" + $content += "`n$(Format-EnvVal 'SMTP_PASS' $c.SmtpPass)" $composeFile = 'docker-compose.yml;docker-compose.saas.yml' if ($c.TlsMode -eq 'custom') { $composeFile += ';docker-compose.tls.yml' } if ($c.MonitoringNetwork) { $composeFile += ';docker-compose.monitoring.yml' } @@ -1017,12 +1028,13 @@ deployment_mode=$($c.DeploymentMode) smtp_host=$($c.SmtpHost) smtp_port=$($c.SmtpPort) smtp_user=$($c.SmtpUser) -smtp_pass=$($c.SmtpPass) smtp_from_email=$($c.SmtpFromEmail) registry=$($c.Registry) registry_user=$($c.RegistryUser) -registry_token=$($c.RegistryToken) "@ + # Passwords appended with single-quoting for special character safety + $txt += "`n$(Format-EnvVal 'smtp_pass' $c.SmtpPass)" + $txt += "`n$(Format-EnvVal 'registry_token' $c.RegistryToken)" Write-Utf8File $f $txt Log-Info 'Saved installer config to cameleer.conf' } diff --git a/install.sh b/install.sh index a8585c9..cb75680 100644 --- a/install.sh +++ b/install.sh @@ -156,6 +156,14 @@ generate_password() { tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32 || : } +# Write KEY='escaped_value' to a file. Single-quotes prevent Docker Compose +# from interpreting $, &, ;, etc. Embedded single quotes are escaped as '\''. +env_val() { + local file="$1" key="$2" val="$3" + val="${val//\'/\'\\\'\'}" + printf "%s='%s'\n" "$key" "$val" >> "$file" +} + # --- Argument parsing --- parse_args() { @@ -259,8 +267,9 @@ load_config_file() { case "$key" in \#*|"") continue ;; esac - key=$(echo "$key" | tr -d ' ') - value=$(echo "$value" | sed 's/^[ ]*//;s/[ ]*$//') + key=$(printf '%s' "$key" | tr -d ' ') + # Trim whitespace, strip surrounding quotes, unescape '\'' + value=$(printf '%s' "$value" | sed "s/^[ ]*//;s/[ ]*$//;s/^'\\(.*\\)'$/\\1/;s/^\"\\(.*\\)\"$/\\1/" | sed "s/'\\\\''/'/g") case "$key" in install_dir) [ -z "$INSTALL_DIR" ] && INSTALL_DIR="$value" ;; public_host) [ -z "$PUBLIC_HOST" ] && PUBLIC_HOST="$value" ;; @@ -663,15 +672,10 @@ HTTPS_PORT=${HTTPS_PORT} # PostgreSQL POSTGRES_USER=cameleer -POSTGRES_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_DB=cameleer -# ClickHouse -CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} - # Server admin SERVER_ADMIN_USER=${ADMIN_USER} -SERVER_ADMIN_PASS=${ADMIN_PASS} # Bootstrap token (required by server, not used externally in standalone mode) BOOTSTRAP_TOKEN=$(generate_password) @@ -694,6 +698,10 @@ SERVER_UI_IMAGE=${REGISTRY}/cameleer-server-ui # Compose file assembly COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml") EOF + # Passwords are appended with single-quoting to handle special characters + env_val "$f" POSTGRES_PASSWORD "$POSTGRES_PASSWORD" + env_val "$f" CLICKHOUSE_PASSWORD "$CLICKHOUSE_PASSWORD" + env_val "$f" SERVER_ADMIN_PASS "$ADMIN_PASS" if [ "$TLS_MODE" = "custom" ]; then echo "CERT_FILE=/user-certs/cert.pem" >> "$f" echo "KEY_FILE=/user-certs/key.pem" >> "$f" @@ -729,15 +737,10 @@ LOGTO_CONSOLE_BIND=$([ "$LOGTO_CONSOLE_EXPOSED" = "true" ] && echo "0.0.0.0" || # PostgreSQL POSTGRES_USER=cameleer -POSTGRES_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_DB=cameleer_saas -# ClickHouse -CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} - # Admin user SAAS_ADMIN_USER=${ADMIN_USER} -SAAS_ADMIN_PASS=${ADMIN_PASS} # TLS NODE_TLS_REJECT=${NODE_TLS_REJECT} @@ -778,13 +781,18 @@ CAMELEER_SERVER_SECURITY_JWTSECRET=$(generate_password) SMTP_HOST=${SMTP_HOST} SMTP_PORT=${SMTP_PORT:-587} SMTP_USER=${SMTP_USER} -SMTP_PASS=${SMTP_PASS} SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL:-noreply@${PUBLIC_HOST}} # Compose file assembly COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml") EOF + # Passwords are appended with single-quoting to handle special characters + env_val "$f" POSTGRES_PASSWORD "$POSTGRES_PASSWORD" + env_val "$f" CLICKHOUSE_PASSWORD "$CLICKHOUSE_PASSWORD" + env_val "$f" SAAS_ADMIN_PASS "$ADMIN_PASS" + env_val "$f" SMTP_PASS "$SMTP_PASS" + if [ -n "$MONITORING_NETWORK" ]; then echo "" >> "$f" echo "# Monitoring" >> "$f" @@ -967,12 +975,13 @@ deployment_mode=${DEPLOYMENT_MODE} smtp_host=${SMTP_HOST} smtp_port=${SMTP_PORT} smtp_user=${SMTP_USER} -smtp_pass=${SMTP_PASS} smtp_from_email=${SMTP_FROM_EMAIL} registry=${REGISTRY} registry_user=${REGISTRY_USER} -registry_token=${REGISTRY_TOKEN} EOF + # Passwords appended with single-quoting for special character safety + env_val "$f" smtp_pass "$SMTP_PASS" + env_val "$f" registry_token "$REGISTRY_TOKEN" log_info "Saved installer config to cameleer.conf" } diff --git a/templates/docker-compose.saas.yml b/templates/docker-compose.saas.yml index 23be1f9..3aa140f 100644 --- a/templates/docker-compose.saas.yml +++ b/templates/docker-compose.saas.yml @@ -9,7 +9,8 @@ services: cameleer-postgres: condition: service_healthy environment: - DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto + # DB_URL is built by the entrypoint from PG_USER/PG_PASSWORD/PG_HOST + # to safely handle special characters in the password ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost} ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002} TRUST_PROXY_HEADER: 1