./discourse-setup place le nom d'utilisateur SMTP au début du mot de passe SMTP

Environnement

  • Discourse : latest tests-passed
  • OS hôte : [Ubuntu 24.04 LTS / 24.04.3 LTS]
  • Méthode d’installation : installation Docker officielle, exécution de ./discourse-setup
  • Plateforme : VPS (Digital Ocean)

Étapes pour reproduire

  1. Exécutez ./discourse-setup depuis /var/discourse
  2. Entrez les détails SMTP lorsqu’ils sont demandés :
  1. Terminez la configuration et inspectez containers/app.yml

Résultat attendu

app.yml devrait contenir deux champs distincts, par exemple :

DISCOURSE_SMTP_USER_NAME: "user@example.com"
DISCOURSE_SMTP_PASSWORD: "p@ssw0rd!"

Résultat actuel

Le champ du mot de passe est écrit avec le nom d’utilisateur préfixé :

DISCOURSE_SMTP_USER_NAME: "user@example.com"
DISCOURSE_SMTP_PASSWORD: "user@brevo.comp@ssw0rd!"

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.

1 « J'aime »

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 ?

1 « J'aime »

Bien. C’est un bon pari.

@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.

Si vous supprimiez la partie 83f…com du mot de passe et que vous laissiez uniquement le mot de passe (5AH…), est-ce que cela fonctionne ?

2 « J'aime »

oui, puis j’exécute .\\launcher rebuild app, puisque .\\discourse-setup avait déjà configuré SWAP

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 :

J’ai reclassé ceci dans Support.

Les noms d’utilisateur SMTP contiennent toujours @, n’est-ce pas ?

Ouais. Je ne pense pas que @ pose problème.

1 « J'aime »

il n’y a pas de ! dans ma capture d’écran là où la concaténation se produit

Quel est le mot de passe que vous essayez d’entrer ?

le mot de passe sur la capture d’écran est 5AHQXrf4LDUmRB1J :slightly_smiling_face:

Modification : ce mot de passe est un mot de passe par défaut de Brevo créé sur un compte qui a depuis été supprimé.

Je n’arrive pas à le reproduire. Quel système d’exploitation utilisez-vous ?

root@bro:/var/discourse# ./discourse-setup --skip-connection-test --skip-rebuild
skipping connection test
'samples/standalone.yml' -> 'containers/app.yml'
Found 29GB of memory and 32 physical CPU cores
setting db_shared_buffers = 4096MB
setting UNICORN_WORKERS = 8
containers/app.yml memory parameters updated.

Hostname for your Discourse? [discourse.example.com]: forum.phsics.site

Setting EC to 2
Skipping port check.
Email address for admin account(s)? [me@example.com,you@example.com]: jay@literatecomputing.com
SMTP server address? [smtp.example.com]: smtp-relay.brevo.com
SMTP port? [587]: 2525
SMTP user name? [user@example.com]: 83fca0012@smtp-brevo.com
SMTP password? []: 5AHQXrf4LDUmRB1J
notification email address? [noreply@forum.phsics.site]:
Optional email address for Let's Encrypt warnings? (ENTER to skip) [me@example.com]:
Optional MaxMind Account ID (ENTER to continue without MAXMIND GeoLite2 geolocation database) [123456]:

Does this look right?

Hostname          : forum.phsics.site
Email             : jay@literatecomputing.com
SMTP address      : smtp-relay.brevo.com
SMTP port         : 2525
SMTP username     : 83fca0012@smtp-brevo.com
SMTP password     : 5AHQXrf4LDUmRB1J
Notification email: noreply@forum.phsics.site
MaxMind account ID: (unset)
MaxMind license key: (unset)

ENTER to continue, 'n' to try again, Ctrl+C to exit:
letsencrypt.ssl.template.yml enabled


Configuration file at containers/app.yml updated successfully!

Updates successful. --skip-rebuild requested. Exiting.
root@bro:/var/discourse# grep SMTP containers/app.yml
  ## TODO: The SMTP mail server used to validate new accounts and send notifications
  # SMTP ADDRESS is required
  # WARNING: SMTP password should be wrapped in quotes to avoid problems
  DISCOURSE_SMTP_ADDRESS: smtp-relay.brevo.com
  DISCOURSE_SMTP_PORT: 2525
  DISCOURSE_SMTP_USER_NAME: 83fca0012@smtp-brevo.com
  DISCOURSE_SMTP_PASSWORD: "5AHQXrf4LDUmRB1J"
  #DISCOURSE_SMTP_ENABLE_START_TLS: true           # (optional, default: true)
  DISCOURSE_SMTP_DOMAIN: discourse.example.com # (required by some providers)
  #DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: peer        # (optional, default: peer, valid values: none, peer, client_once, fail_if_no_peer_cert)
  #DISCOURSE_SMTP_AUTHENTICATION: plain            # (default: plain, valid values: plain, login, cram_md5)

Digital Ocean Ubuntu 24.04

Recommandations générées par LLM/IA

\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.

2 « J'aime »
également généré par LLM/IA

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 :

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

… avec des valeurs entre guillemets doubles sûres pour YAML et des remplacements ancrés.

Alors je n’arrive pas à imaginer comment c’est arrivé. Pouvez-vous le reproduire comme dans mon exemple ?

Avez-vous copié/collé tout le texte en une seule fois, de sorte que le tampon n’ait pas pu suivre la vitesse à laquelle vous colliez ?

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.

1 « J'aime »