Ciò causa il mancato recapito della posta e discourse-doctor conferma la password alterata.
Note
• La citazione esplicita della password al prompt non cambia l’esito.
• La password contiene caratteri speciali (@, !), ma il problema non è solo il quoting YAML: la stringa del nome utente viene letteralmente concatenata all’inizio della password.
• Riproducibile su più esecuzioni di ./discourse-setup.
Sì. Quello è il problema. Ci sono più livelli di escaping che devono avvenire, come quando bash legge il valore, quando bash passa il valore a sed, quando sed lo sostituisce, e poi, forse, quando il file yml lo riceve. È un problema noto:
\nqui sotto c’è una patch mirata che:\n\n- smette di usare sed\n- costruisce una SMTP_URL codificata in percentuale\n- modifica containers/app.yml tramite YAML di Ruby (Psych), quindi l’escape/quoting YAML è gestito da un vero parser\n- elimina le variabili SMTP per chiave per evitare contraddizioni\n\napplicare con git apply -p0 nella repository discourse_docker.\n\n- - -\n\nPatch 1 -\n\ndiscourse-setup\n\n(scrive SMTP usando Ruby YAML, non sed)\n\ndiff\n--- a/discourse-setup\n+++ b/discourse-setup\n@@ -867,6 +867,77 @@ write_smtp_settings() {\n local app_yml=\"containers/app.yml\"\n [[ -f \"$app_yml\" ]] || die \"Cannot find $app_yml. Did you run bootstrap?\"\n\n+ # Costruisce una SMTP_URL codificata in URL usando la libreria standard Python (nessun gioco di escape della shell)\n+ urlencode() {\n+ python3 - \u003c\u003c'PY'\n+import sys, urllib.parse\n+print(urllib.parse.quote(sys.stdin.read().strip(), safe='._~-'))\n+PY\n+ }\n+\n+ # IMPORTANTE: legge le variabili senza manipolazione di backslash\n+ # (queste provengono da prompt precedenti; assicurarsi solo che -r sia usato al momento del prompt)\n+ local addr=\"$smtp_address\"\n+ local port=\"$smtp_port\"\n+ local user_enc pass_enc\n+ user_enc=\"$(printf '%s' \"$smtp_user\" | urlencode)\"\n+ pass_enc=\"$(printf '%s' \"$smtp_password\" | urlencode)\"\n+ local smtp_url=\"smtp://${user_enc}:${pass_enc}@${addr}:${port}\"\n+\n+ # Usa Ruby per caricare/modificare/salvare in modo sicuro YAML (elimina 3 livelli di escaping)\n+ ruby - \u003c\u003c'RUBY' \"$app_yml\" \"$smtp_url\"\n+require \"yaml\"\n+require \"psych\"\n+path, url = ARGV\n+doc = YAML.safe_load(File.read(path), permitted_classes: [], aliases: true) || {}\n+\n+# Assicura che la struttura di primo livello sia un Hash e abbia env\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml non viene analizzato come Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Scrive SMTP_URL su una sola riga; rimuove le variabili per chiave per evitare conflitti\n+env[\"SMTP_URL\"] = url\n+%w[DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_PORT DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD].each { |k| env.delete(k) }\n+\n+# Scarica indietro. (Psych conserva le stringhe correttamente quotate secondo necessità.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # rapido controllo di sanità per il classico fallimento "password preceduta da username"\n+ python3 - \u003c\u003c'PY'\n+import re, sys\n+y = open(\"containers/app.yml\",\"r\",encoding=\"utf-8\").read()\n+m = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)\n+assert m, \"SMTP_URL mancante dopo la scrittura\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"Credenziali SMTP_URL mancanti ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"La password sembra preceduta dall'username\"\n+print(\"SMTP_URL sembra sensato.\")\n+PY\n+}\n+\n- # Scrive voci SMTP per chiave (indirizzo/porta/username/password)\n- # (legacy: eseguito tramite sostituzioni sed)\n- # NOTA: storicamente fragile con caratteri speciali\n- update_setting_yaml \"DISCOURSE_SMTP_ADDRESS\" \"$smtp_address\"\n- update_setting_yaml \"DISCOURSE_SMTP_PORT\" \"$smtp_port\"\n- update_setting_yaml \"DISCOURSE_SMTP_USER_NAME\" \"$smtp_user\"\n- update_setting_yaml \"DISCOURSE_SMTP_PASSWORD\" \"$smtp_password\"\n-}\n+ # (scritture legacy per chiave rimosse a favore di SMTP_URL tramite YAML)\n+}\n\n\npoi Patch 2 -\n\ntemplates/web.template.yml\n\n(per documentare il percorso più sicuro)\n\ndiff\n--- a/templates/web.template.yml\n+++ b/templates/web.template.yml\n@@ -68,6 +68,14 @@ params:\n DISCOURSE_SMTP_ENABLE_START_TLS: true\n #DISCOURSE_NOTIFICATION_EMAIL: noreply@example.com\n\n+ ## Configurazione SMTP preferita su una sola riga (impostata da discourse-setup):\n+ ## Codifica in URL username e password; esempio:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Se non puoi usare SMTP_URL, puoi impostare variabili per chiave invece.\n ## Attenzione che la modifica di quelle righe con strumenti da riga di comando può essere fragile se i valori includono\n ## caratteri come @, :, /, \", \\, o nuove righe.\n\n\nperché funziona (e cosa evita)\n\t•\tlayer bash: interpoliamo solo variabili semplici; i segreti vengono passati a Python/Ruby tramite stdin/argv, non tramite regex sed o eval della shell.\n\t•\tlayer sed: rimosso completamente.\n\t•\tlayer YAML: Ruby/Psych gestisce correttamente il quoting e l’escape; nessun quoting fatto a mano.\n\t•\tcredenziali SMTP: la codifica % in SMTP_URL è il posto giusto per codificare caratteri speciali per l’autenticazione.\n\nse preferisci mantenere le variabili per chiave, posso fornirti una patch sorella che usa lo stesso approccio Ruby-YAML per impostare direttamente DISCOURSE_SMTP_* (ancora senza sed), ma il percorso SMTP_URL è il più pulito perché è una chiave, una scrittura, un passaggio di codifica.\n
Purtroppo non possiamo fare affidamento sull’installazione di Ruby sul sistema host nelle installazioni di discourse: sarebbe relativamente semplice far sì che l’intero script sia in ruby, se potessimo, ma non c’è alcuna garanzia.
Ricevuto: niente Ruby/Python sull’host. Ecco una patch pura Bash + awk che:
• costruisce una SMTP_URL codificata in percentuale usando solo Bash (ciclo byte per byte; niente Python),
• inserisce o sostituisce SMTP_URL nel blocco env: usando awk (niente sed),
• rimuove le righe DISCOURSE_SMTP_* per chiave (eliminazioni ancorate sicure),
• aggiunge un piccolo controllo di sanità usando solo grep/awk.
Applica con git apply -p0 nella repository discourse_docker.
--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,130 @@ write_smtp_settings() {
local app_yml="containers/app.yml"
[[ -f "$app_yml" ]] || die "Impossibile trovare $app_yml. Hai eseguito il bootstrap?"
+ ##############################################
+ # Codificatore URL puro Bash per credenziali SMTP #
+ ##############################################
+ # Codifica tutto tranne A-Z a-z 0-9 . _ ~ -
+ # Funziona byte per byte; richiede bash e printf.
+ urlencode_cred() {
+ local s="$1" out= i ch o
+ # imposta la locale C per ottenere semantica dei byte
+ LC_ALL=C
+ for ((i=0; i<${#s}; i++)); do
+ ch="${s:i:1}"
+ case "$ch" in
+ [A-Za-z0-9._~-])
+ out+="$ch"
+ ;;
+ *)
+ # Ottieni valore byte: stampa carattere, leggi con od, poi formatta %HH
+ # Evita dipendenze pesanti esterne; od è in coreutils / busybox.
+ o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+ # Se od ha fallito in qualche modo (vuoto), torna all'esadecimale tramite printf %02X del primo byte
+ if [ -z "$o" ]; then
+ o=$(printf '%s' "$ch" | head -c1 | od -An -tu1 | awk '{$1=$1;print $1}')
+ fi
+ printf -v o '%%%02X' "$o"
+ out+="$o"
+ ;;
+ esac
+ done
+ printf '%s' "$out"
+ }
+
+ # Costruisci SMTP_URL (singola riga) dalle risposte raccolte
+ # Queste variabili sono raccolte in precedenza in discourse-setup:
+ # $smtp_address $smtp_port $smtp_user $smtp_password
+ local addr="$smtp_address"
+ local port="$smtp_port"
+ local user_enc pass_enc
+ user_enc="$(urlencode_cred "$smtp_user")"
+ pass_enc="$(urlencode_cred "$smtp_password")"
+ local smtp_url="smtp://${user_enc}:${pass_enc}@${addr}:${port}"
+
+ ########################################################
+ # Modifica sicura per YAML tramite awk (niente sed / niente runtime esterni)
+ # - assicurati che esista env:
+ # - inserisci o sostituisci SMTP_URL sotto env:
+ # - rimuovi le chiavi DISCOURSE_SMTP_*
+ ########################################################
+ awk -v NEWVAL="$smtp_url" '
+ BEGIN{
+ have_env=0; in_env=0; inserted=0
+ }
+ # rileva la riga env: al livello superiore (inizio riga, possibilmente indentata 0..)
+ # considereremo un'indentazione di due spazi per i figli.
+ /^[[:space:]]*env:[[:space:]]*$/ {
+ print; have_env=1; in_env=1; next
+ }
+ # uscendo dal blocco env: quando l'indentazione torna a 0 o alla prossima chiave di livello superiore
+ in_env && /^[^[:space:]]/ {
+ if (!inserted) {
+ print " SMTP_URL: \"' NEWVAL '\""
+ inserted=1
+ }
+ in_env=0
+ }
+ # mentre in env:, gestisci sostituzioni ed eliminazioni
+ in_env {
+ # elimina interamente le righe DISCOURSE_SMTP_* per chiave
+ if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+ # sostituisci la riga SMTP_URL esistente
+ if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+ print " SMTP_URL: \"' NEWVAL '\""
+ inserted=1
+ next
+ }
+ print
+ next
+ }
+ { print }
+ END{
+ # Se env: non è mai esistito, aggiungilo con la chiave
+ if (!have_env) {
+ print ""
+ print "env:"
+ print " SMTP_URL: \"' NEWVAL '\""
+ } else if (in_env && !inserted) {
+ # env: è esistito e ci trovavamo ancora lì alla fine del file
+ print " SMTP_URL: \"' NEWVAL '\""
+ }
+ }
+ ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+ ##############################################
+ # Controllo di sanità: guardia di base contro la manomissione #
+ ##############################################
+ # 1) SMTP_URL presente
+ grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL non scritto su $app_yml"
+ # 2) la password non è preceduta dal nome utente (firma classica di errore)
+ awk '
+ BEGIN{ok=1}
+ /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+ line=$0
+ gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+ gsub(/".*$/,"",line)
+ # Estrai le credenziali prima di @ e dopo lo schema
+ # es. smtp://user:pass@host:port
+ sub(/^[a-z]+:\/\//,"",line)
+ at=index(line,"@")
+ if (at>0) {
+ creds=substr(line,1,at-1)
+ colon=index(creds,":")
+ if (colon>0) {
+ user=substr(creds,1,colon-1)
+ pass=substr(creds,colon+1)
+ if (index(pass,user)==1) { ok=0 }
+ }
+ }
+ }
+ END{ if (!ok) { print "La password sembra preceduta dal nome utente"; exit 1 } }
+ ' "$app_yml"
+}
+
+ # Rimozione delle vecchie scritture per chiave
+ # (legacy: eseguite tramite sostituzioni sed)
+ # NOTA: storicamente fragili con caratteri speciali
+ # update_setting_yaml "DISCOURSE_SMTP_ADDRESS" "$smtp_address"
+ # update_setting_yaml "DISCOURSE_SMTP_PORT" "$smtp_port"
+ # update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
+ # update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
+
+ # Scritture per chiave SMTP per chiave (indirizzo/porta/nome utente/password)
+ # (legacy: eseguite tramite sostituzioni sed)
+ # NOTA: storicamente fragili con caratteri speciali
+ # update_setting_yaml "DISCOURSE_SMTP_ADDRESS" "$smtp_address"
+ # update_setting_yaml "DISCOURSE_SMTP_PORT" "$smtp_port"
+ # update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
+ # update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
+# Rimozione delle vecchie scritture per chiave in favore della scrittura atomica SMTP_URL sopra.
+
+
- # Write per-key SMTP entries (address/port/username/password)
- # (legacy: performed via sed substitutions)
- # NOTE: historically fragile with special chars
- update_setting_yaml "DISCOURSE_SMTP_ADDRESS" "$smtp_address"
- update_setting_yaml "DISCOURSE_SMTP_PORT" "$smtp_port"
- update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
- update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
-}
+ # Scritture per chiave SMTP per chiave (indirizzo/porta/nome utente/password)
+ # (legacy: eseguite tramite sostituzioni sed)
+ # NOTA: storicamente fragili con caratteri speciali
+ # update_setting_yaml "DISCOURSE_SMTP_ADDRESS" "$smtp_address"
+ # update_setting_yaml "DISCOURSE_SMTP_PORT" "$smtp_port"
+ # update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
+ # update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
+
+# Rimozione delle vecchie scritture per chiave in favore della scrittura atomica SMTP_URL sopra.
+}
Perché questo soddisfa il tuo vincolo
• Zero Ruby/Python sull’host: solo Bash + awk + od/printf/grep (tutti standard).
• Nessuna sostituzione sed di segreti: evitiamo la trappola multi-escape “Bash → sed → YAML”.
• Scrittura atomica-ish: modifica in un file temporaneo, poi mv su containers/app.yml.
• Retrocompatibile: se env: non esiste, lo creiamo in modo minimale; se esiste, lo aggiorniamo sul posto e rimuoviamo le righe DISCOURSE_SMTP_* in conflitto.
• Le credenziali vengono codificate in URL prima dell’inserimento, quindi i caratteri speciali in utente/password non interromperanno il parser URL in Rails/Net::SMTP.
Se preferisci mantenere le variabili per chiave invece di SMTP_URL, posso inviare una patch sorella che (ancora senza sed) utilizza la stessa strategia awk per impostare:
hmmm mi chiedo se possiamo semplicemente includere la logica in launcher2, quindi possiamo usarla per eseguire l’installazione.
La quantità di ostacoli che discourse-setup sta superando è spettacolare.
È possibile compilare il launcher in un file eseguibile binario per il rilascio? In questo modo, il launcher (bash) potrà scaricare solo il file eseguibile (binario) corrispondente in base al sistema.