Cela entraîne l’échec de la livraison des e-mails, et discourse-doctor confirme le mot de passe corrompu.
Notes
• Mettre le mot de passe entre guillemets explicitement à l’invite ne change pas le résultat.
• Le mot de passe contient des caractères spéciaux (@, !), mais le problème n’est pas seulement le YAML quoting : la chaîne du nom d’utilisateur est littéralement concaténée au début du mot de passe.
• Reproductible sur plusieurs exécutions de ./discourse-setup.
Pouvez-vous tester si cela est lié à des lettres particulières dans le mot de passe, peut-être @ / ! cela révèle-t-il le bug ?
Tous les scripts sed/awk et les scripts bash sophistiqués de discourse-setup le rendent plutôt difficile à maintenir. Peut-être que @pfaffman a des idées à ce sujet ?
@Ethsim2 si vous changez le mot de passe pour quelque chose avec seulement des chiffres et des lettres et pas le même que celui d’une partie du nom d’utilisateur, avez-vous ce problème ?
Pouvez-vous partager les valeurs que vous utilisez ?
J’essaierai de jeter un œil plus tard aujourd’hui.
Ouais. C’est ça le problème. Il y a plusieurs niveaux d’échappement qui doivent se produire, comme lorsque bash lit la valeur, lorsque bash transmet la valeur à sed, lorsque sed la remplace, et puis, peut-être, lorsque le fichier yml la reçoit. C’est un problème connu :
\nVoici un correctif ciblé qui :\n\n- arrête d’utiliser sed\n- construit une URL SMTP encodée en pourcentage\n- modifie containers/app.yml via le module YAML (Psych) de Ruby, de sorte que l’échappement/le codage YAML est géré par un véritable analyseur\n- supprime les variables SMTP par clé pour éviter les contradictions\n\nAppliquez avec git apply -p0 dans le dépôt discourse_docker.\n\n- - -\n\nCorrectif 1 -\n\ndiscourse-setup\n\n(écrit SMTP en utilisant YAML Ruby, pas 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+ # Construit une URL SMTP encodée en URL à l'aide de la bibliothèque standard Python (pas de jeux d'échappement 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+ # IMPORTANT : lit les variables sans permutation des antislashs\n+ # (celles-ci proviennent de invites précédentes ; assurez-vous simplement que -r est utilisé au moment de l'invite)\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+ # Utilise Ruby pour charger/modifier/sauvegarder YAML en toute sécurité (supprime 3 couches d'échappement)\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+# Assure que la structure de niveau supérieur est un Hash et a env\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml ne se parse pas en Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Écrit SMTP_URL sur une seule ligne ; supprime les variables par clé pour éviter les conflits\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+# Sauvegarde. (Psych préserve les chaînes correctement échappées si nécessaire.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # vérification rapide pour l'échec classique « mot de passe préfixé par le nom d'utilisateur »\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 manquant après écriture\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"Identifiants SMTP_URL manquants ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Le mot de passe semble préfixé par le nom d'utilisateur\"\n+print(\"SMTP_URL semble correct.\")\n+PY\n+}\n+\n- # Écrit les entrées SMTP par clé (adresse/port/nom d'utilisateur/mot de passe)\n- # (hérité : effectué via des substitutions sed)\n- # NOTE : historiquement fragile avec les caractères spéciaux\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+ # (écritures héritées par clé supprimées au profit de SMTP_URL via YAML)\n+}\n\n\npuis Correctif 2 -\n\ntemplates/web.template.yml\n\n(pour documenter le chemin plus sûr)\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+ ## Configuration SMTP préférée sur une seule ligne (définie par discourse-setup) :\n+ ## Encodez le nom d'utilisateur et le mot de passe en URL ; exemple :\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Si vous ne pouvez pas utiliser SMTP_URL, vous pouvez définir des variables par clé à la place.\n ## Attention, la modification de ces lignes avec des outils shell peut être fragile si les valeurs incluent\n ## des caractères tels que @, :, /, \", \\ ou des sauts de ligne.\n\n\nPourquoi cela fonctionne (et ce que cela évite)\n\t•\tcouche bash : nous n’intercalons que des variables simples ; les secrets sont passés à Python/Ruby via stdin/argv, pas via des expressions régulières sed ou des évaluations shell.\n\t•\tcouche sed : entièrement supprimée.\n\t•\tcouche YAML : Ruby/Psych gère correctement le codage et l’échappement ; pas de codage manuel.\n\t•\tidentifiants SMTP : l’encodage % dans SMTP_URL est le bon endroit pour encoder les caractères spéciaux pour l’authentification.\n\nSi vous préférez conserver les variables par clé, je peux vous fournir un correctif sœur qui utilise la même approche Ruby-YAML pour définir directement DISCOURSE_SMTP_* (toujours sans sed), mais la voie SMTP_URL est la plus propre car c’est une clé, une écriture, une étape d’encodage.\n
Nous ne pouvons malheureusement pas compter sur l’installation de Ruby sur le système hôte dans les installations de discourse. Ce serait relativement simple d’avoir le script entier en Ruby si nous le pouvions, mais il n’y a aucune garantie.
Compris — pas de Ruby/Python sur l’hôte. Voici un patch purement Bash + awk qui :
• construit une URL SMTP_URL encodée en pourcentage en utilisant uniquement Bash (boucle octet par octet ; pas de Python),
• insère ou remplace SMTP_URL sous le bloc env: en utilisant awk (pas de sed),
• supprime les lignes DISCOURSE_SMTP_* par clé (suppressions ancrées sûres),
• ajoute une petite vérification de bon sens en utilisant uniquement grep/awk.
Appliquez avec git apply -p0 dans le dépôt 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 "Impossible de trouver $app_yml. Avez-vous exécuté bootstrap ?"
+ ##############################################
+ # Encodeur d'URL Bash pur pour les identifiants SMTP #
+ ##############################################
+ # Encode tout sauf A-Z a-z 0-9 . _ ~ -
+ # Fonctionne octet par octet ; nécessite bash et printf.
+ urlencode_cred() {
+ local s="$1" out= i ch o
+ # définit la locale C pour obtenir la sémantique des octets
+ LC_ALL=C
+ for ((i=0; i<${#s}; i++)); do
+ ch="${s:i:1}"
+ case "$ch" in
+ [A-Za-z0-9._~-])
+ out+="$ch"
+ ;;
+ *)
+ # Obtient la valeur de l'octet : imprime le caractère, lit avec od, puis formate %HH
+ # Évite les dépendances externes lourdes ; od est dans coreutils / busybox.
+ o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+ # Si od a échoué (vide), revenir au format hexadécimal via printf %02X du premier octet
+ 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"
+ }
+
+ # Construit SMTP_URL (une seule ligne) à partir des réponses collectées
+ # Ces variables sont collectées plus tôt dans 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}"
+
+ ########################################################
+ # Modification sûre pour YAML via awk (pas de sed / pas de runtimes externes)
+ # - s'assurer que env: existe
+ # - insérer ou remplacer SMTP_URL sous env:
+ # - supprimer les clés DISCOURSE_SMTP_*
+ ########################################################
+ awk -v NEWVAL="$smtp_url" '
+ BEGIN{
+ have_env=0; in_env=0; inserted=0
+ }
+ # détecte la ligne env: au niveau supérieur (début de ligne, éventuellement indentée de 0..)
+ # nous considérerons une indentation de deux espaces pour les enfants.
+ /^[[:space:]]*env:[[:space:]]*$/ {
+ print; have_env=1; in_env=1; next
+ }
+ # quitte le bloc env: lorsque l'indentation revient à 0 ou à la clé de niveau supérieur suivante
+ in_env && /^[^[:space:]]/ {
+ if (!inserted) {
+ print " SMTP_URL: \"' NEWVAL '\""
+ inserted=1
+ }
+ in_env=0
+ }
+ # pendant que dans env:, gérer les remplacements et les suppressions
+ in_env {
+ # supprime complètement les lignes DISCOURSE_SMTP_* par clé
+ if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+ # remplace la ligne SMTP_URL existante
+ if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+ print " SMTP_URL: \"' NEWVAL '\""
+ inserted=1
+ next
+ }
+ print
+ next
+ }
+ { print }
+ END{
+ # Si env: n'a jamais existé, ajoutez-le avec la clé
+ if (!have_env) {
+ print ""
+ print "env:"
+ print " SMTP_URL: \"' NEWVAL '\""
+ } else if (in_env && !inserted) {
+ # env: existait et nous étions toujours dedans à la fin du fichier
+ print " SMTP_URL: \"' NEWVAL '\""
+ }
+ }
+ ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+ ##############################################
+ # Vérification de bon sens : garde de base contre le déchirement #
+ ##############################################
+ # 1) SMTP_URL présent
+ grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL non écrit dans $app_yml"
+ # 2) le mot de passe n'est pas préfixé par le nom d'utilisateur (signature d'échec classique)
+ awk '
+ BEGIN{ok=1}
+ /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+ line=$0
+ gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+ gsub(/".*$/,"",line)
+ # Extrait les identifiants avant @ et après le schéma
+ # par exemple 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 "Le mot de passe semble être préfixé par le nom d'utilisateur"; exit 1 } }
+ ' "$app_yml"
+ }
+
##############################################
# Pure-Bash URL encoder for SMTP credentials #
##############################################
@@ -995,17 +1119,6 @@
# 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"
-}
-
-
-- # 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"
-}
+ # Les écritures héritées par clé ont été supprimées en faveur de l'écriture atomique SMTP_URL ci-dessus.
+}
Pourquoi cela répond à votre contrainte
• Zéro Ruby/Python sur l’hôte : uniquement Bash + awk + od/printf/grep (tout est standard).
• Pas de remplacement de secrets par sed : nous évitons le piège multi-échappement “Bash → sed → YAML”.
• Écriture atomique (ou presque) : modification dans un fichier temporaire, puis déplacement sur containers/app.yml.
• Sûr pour la rétrocompatibilité : si env: n’existe pas, nous le créons de manière minimale ; s’il existe, nous le mettons à jour sur place et supprimons les lignes conflictuelles DISCOURSE_SMTP_*.
• Les identifiants sont encodés en URL avant l’insertion, de sorte que les caractères spéciaux dans l’utilisateur/mot de passe ne casseront pas l’analyseur d’URL dans Rails/Net::SMTP.
Si vous préférez conserver les variables par clé au lieu de SMTP_URL, je peux envoyer un patch frère qui (toujours sans sed) utilise la même stratégie awk pour définir :
hmmm je me demande si nous pouvons simplement regrouper la logique dans launcher2, puis nous pouvons l’utiliser pour exécuter la configuration.
Le nombre de pirouettes que discourse-setup effectue est spectaculaire.
Est-il possible de compiler le lanceur en un fichier exécutable binaire pour la publication ? De cette façon, le lanceur (bash) ne pourra télécharger que le fichier exécutable (binaire) correspondant en fonction du système.