./discourse-setup setzt SMTP-Benutzernamen an den Anfang des SMTP-Passworts

Umgebung

  • Discourse: latest tests-passed
  • Host-Betriebssystem: [Ubuntu 24.04 LTS / 24.04.3 LTS]
  • Installationsmethode: Offizielle Docker-Installation, Ausführung von ./discourse-setup
  • Plattform: VPS (Digital Ocean)

Schritte zur Reproduktion

  1. Führen Sie ./discourse-setup von /var/discourse aus.
  2. Geben Sie die SMTP-Details ein, wenn Sie dazu aufgefordert werden:
  1. Schließen Sie die Einrichtung ab und überprüfen Sie containers/app.yml.

Erwartetes Ergebnis

app.yml sollte zwei separate Felder enthalten, zum Beispiel:

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

Tatsächliches Ergebnis

Das Passwortfeld wird mit dem vorangestellten Benutzernamen geschrieben:

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

Dies führt zu einem Fehlschlag bei der E-Mail-Zustellung, und discourse-doctor bestätigt das fehlerhafte Passwort.

Hinweise
• Das explizite Anführungszeichen für das Passwort bei der Eingabeaufforderung ändert das Ergebnis nicht.
• Das Passwort enthält Sonderzeichen (@, !), aber das Problem ist nicht nur die YAML-Anführungszeichen: Der Benutzername wird buchstäblich am Anfang des Passworts verkettet.
• Reproduzierbar bei mehreren Ausführungen von ./discourse-setup.

1 „Gefällt mir“

Kannst du testen, ob dies mit bestimmten Zeichen im Passwort zusammenhängt, vielleicht @ / ! den Fehler aufdeckt?

Die ganze sed/awk und das schicke Bash-Scripting in discourse-setup machen es ziemlich schwierig zu warten. Vielleicht hat @pfaffman hier ein paar Ideen?

1 „Gefällt mir“

Okay. Das ist eine gute Vermutung.

@Ethsim2, wenn Sie das Passwort in etwas ändern, das nur Zahlen und Buchstaben enthält und nicht dasselbe wie ein Teil des Benutzernamens ist, haben Sie dann dieses Problem?

Können Sie die verwendeten Werte mitteilen?

Ich werde versuchen, es mir später am Tag anzusehen.

Wenn Sie den Teil 83f…com aus dem Passwort entfernt und nur das Passwort (5AH…) belassen, funktioniert es dann?

2 „Gefällt mir“

Ja, dann führe ich .\\launcher rebuild app aus, da .\\discourse-setup SWAP bereits konfiguriert hatte

Ja. Das ist das Problem. Es gibt mehrere Eskalationsstufen, die stattfinden müssen, z. B. wenn bash den Wert liest, wenn bash den Wert an sed übergibt, wenn sed ihn ersetzt und dann vielleicht, wenn die yml-Datei ihn erhält. Es ist ein bekanntes Problem:

Ich habe dies als Support neu kategorisiert.

SMTP-Benutzernamen enthalten doch immer @, oder?

Ich glaube nicht, dass @ ein Problem verursachen sollte.

1 „Gefällt mir“

In meinem Screenshot gibt es kein ! dort, wo die Verkettung stattfindet.

Was ist das Passwort, das Sie einzugeben versuchen?

[quote=„pfaffman, Beitrag:11, Thema:381710”]
Was ist das Passwort, das Sie einzugeben versucht haben?
[/quote]

Das Passwort auf dem Screenshot lautet 5AHQXrf4LDUmRB1J

Ich kann es nicht reproduzieren. Welches Betriebssystem verwendest du?

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)
1 „Gefällt mir“

Digital Ocean Ubuntu 24.04

Von LLM/KI generierte Empfehlungen

Unten ist ein kleiner Patch, der:

  • sed nicht mehr verwendet
  • eine prozentkodierte SMTP_URL erstellt
  • containers/app.yml über Ruby’s YAML (Psych) bearbeitet, sodass YAML-Anführungszeichen/Escaping von einem echten Parser behandelt werden
  • die SMTP-Variablen pro Schlüssel löscht, um Widersprüche zu vermeiden

Anwenden mit git apply -p0 im discourse_docker-Repository.


Patch 1 -

discourse-setup

(SMTP mit Ruby YAML schreiben, nicht mit sed)

--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,77 @@ write_smtp_settings() {
   local app_yml="containers/app.yml"
   [[ -f "$app_yml" ]] || die "Cannot find $app_yml. Did you run bootstrap?"

+  # URL-kodierte SMTP_URL mit Python-Standardbibliothek erstellen (keine Shell-Escaping-Spielchen)
+  urlencode() {
+    python3 - <<'PY'
+import sys, urllib.parse
+print(urllib.parse.quote(sys.stdin.read().strip(), safe='._~-'))
+PY
+  }
+
+  # WICHTIG: Variablen ohne Backslash-Veränderung lesen
+  # (diese stammen aus früheren Prompts; stellen Sie einfach sicher, dass -r zur Prompt-Zeit verwendet wird)
+  local addr="$smtp_address"
+  local port="$smtp_port"
+  local user_enc pass_enc
+  user_enc="$(printf '%s' "$smtp_user"     | urlencode)"
+  pass_enc="$(printf '%s' "$smtp_password" | urlencode)"
+  local smtp_url="smtp://${user_enc}:${pass_enc}@${addr}:${port}"
+
+  # Ruby verwenden, um YAML sicher zu laden/modifizieren/dumpen (tötet 3 Ebenen des Escapings)
+  ruby - <<'RUBY' "$app_yml" "$smtp_url"
+require "yaml"
+require "psych"
+path, url = ARGV
+doc = YAML.safe_load(File.read(path), permitted_classes: [], aliases: true) || {}
+
+# Sicherstellen, dass die oberste Struktur ein Hash ist und env enthält
+unless doc.is_a?(Hash)
+  abort "containers/app.yml parst nicht zu einem Hash"
+end
+doc["env"] ||= {}
+env = doc["env"]
+
+# Einzelzeilige SMTP_URL schreiben; pro Schlüssel entfernte Variablen, um Konflikte zu vermeiden
+env["SMTP_URL"] = url
+%w[DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_PORT DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD].each { |k| env.delete(k) }
+
+# Zurückschreiben. (Psych bewahrt Zeichenketten sicher kodiert, wie benötigt.)
+File.write(path, Psych.dump(doc))
+RUBY
+
+  # Schnelle Integritätsprüfung für das klassische Fehlschlagen "Passwort mit Benutzernamen als Präfix"
+  python3 - <<'PY'
+import re, sys
+y = open("containers/app.yml","r",encoding="utf-8").read()
+m = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)
+assert m, "SMTP_URL nach dem Schreiben fehlt"
+creds = m.group(1).split('@',1)[0].split('://',1)[-1]
+assert ":" in creds, "SMTP_URL-Anmeldedaten fehlen ':'"
+u, p = creds.split(':',1)
+assert not p.startswith(u), "Passwort scheint mit Benutzernamen als Präfix versehen zu sein"
+print("SMTP_URL sieht vernünftig aus.")
+PY
+}
+
-  # Schreibe SMTP-Einträge pro Schlüssel (Adresse/Port/Benutzername/Passwort)
-  # (Legacy: durchgeführt über sed-Substitutionen)
-  # HINWEIS: Historisch fragil bei Sonderzeichen
-  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"
-}
+  # (Legacy-Schreibvorgänge pro Schlüssel zugunsten von SMTP_URL über YAML entfernt)
+}

dann Patch 2 -

templates/web.template.yml

(um den sichereren Weg zu dokumentieren)

--- a/templates/web.template.yml
+++ b/templates/web.template.yml
@@ -68,6 +68,14 @@ params:
   DISCOURSE_SMTP_ENABLE_START_TLS: true
   #DISCOURSE_NOTIFICATION_EMAIL: noreply@example.com

+  ## Bevorzugte einzeilige SMTP-Konfiguration (gesetzt durch discourse-setup):
+  ## URL-kodieren Sie Benutzername und Passwort; Beispiel:
+  ##   SMTP_URL: "smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587"
+  ##
+  #SMTP_URL:
+
   ## Wenn Sie SMTP_URL nicht verwenden können, können Sie stattdessen Variablen pro Schlüssel setzen.
   ## Beachten Sie, dass die Bearbeitung dieser Zeilen mit Shell-Tools fragil sein kann, wenn Werte enthalten
   ## Zeichen wie @, :, /, ", \, oder Zeilenumbrüche.

Warum das funktioniert (und was es vermeidet)
• Bash-Ebene: Wir interpolieren nur einfache Variablen; Geheimnisse werden über stdin/argv an Python/Ruby übergeben, nicht über sed-Regexes oder Shell-Evals.
• Sed-Ebene: vollständig entfernt.
• YAML-Ebene: Ruby/Psych behandelt Anführungszeichen und Escaping ordnungsgemäß; keine handgefertigten Anführungszeichen.
• SMTP-Anmeldedaten: %-Kodierung in SMTP_URL ist der richtige Ort, um Sonderzeichen für die Authentifizierung zu kodieren.

Wenn Sie lieber Variablen pro Schlüssel beibehalten möchten, kann ich Ihnen einen Schwester-Patch geben, der denselben Ruby-YAML-Ansatz verwendet, um DISCOURSE_SMTP_* direkt zu setzen (immer noch kein sed), aber der SMTP_URL-Weg ist der sauberste, da es sich um einen Schlüssel, einen Schreibvorgang und einen Kodierungsschritt handelt.

Wir können uns leider nicht darauf verlassen, dass Ruby auf dem Hostsystem in Discourse-Installationen installiert ist – es wäre relativ einfach, das gesamte Skript zu einem Ruby-Skript zu machen, wenn wir könnten, aber es gibt keine Garantie dafür.

2 „Gefällt mir“
auch LLM/KI-generiert

Verstanden – kein Ruby/Python auf dem Host. Hier ist ein reiner Bash + awk Patch, der:
• eine prozent-kodierte SMTP_URL nur mit Bash erstellt (Byte-für-Byte-Schleife; kein Python),
• SMTP_URL im env:-Block mit awk einfügt oder ersetzt (kein sed),
• die pro Schlüssel DISCOURSE_SMTP_* Zeilen entfernt (sichere verankerte Löschungen),
• eine kleine Plausibilitätsprüfung nur mit grep/awk hinzufügt.

Anwendung mit git apply -p0 im discourse_docker Repository.

--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,130 @@ write_smtp_settings() {
   local app_yml="containers/app.yml"
   [[ -f "$app_yml" ]] || die "Cannot find $app_yml. Did you run bootstrap?"

+  ##############################################
+  # Pure-Bash URL encoder for SMTP credentials #
+  ##############################################
+  # Encodes everything except A-Z a-z 0-9 . _ ~ -
+  # Works byte-by-byte; requires bash and printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # set C locale to get byte semantics
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Get byte value: print char, read with od, then format %HH
+          # Avoid external heavy deps; od is in coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # If od somehow failed (empty), fall back to hex via printf %02X of first 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"
+  }
+
+  # Build SMTP_URL (single line) from collected answers
+  # These vars are gathered earlier 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}"
+
+  ########################################################
+  # YAML-safe edit via awk (no sed / no external runtimes)
+  # - ensure env: exists
+  # - insert or replace SMTP_URL under env:
+  # - remove DISCOURSE_SMTP_* keys
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # detect env: line at top level (start of line, possibly indented 0..)
+    # we’ll consider two-space indentation for children.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # leaving env: block when indentation returns to 0 or next top-level key
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"' NEWVAL '\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # while in env:, handle replacements and deletions
+    in_env {
+      # drop per-key DISCOURSE_SMTP_* lines entirely
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # replace existing SMTP_URL line
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"' NEWVAL '\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # If env: never existed, append it with the key
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"' NEWVAL '\""
+      } else if (in_env && !inserted) {
+        # env: existed and we were still in it at EOF
+        print "  SMTP_URL: \"' NEWVAL '\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Sanity check: basic guard against mangling #
+  ##############################################
+  # 1) SMTP_URL present
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL not written to $app_yml"
+  # 2) password not prefixed by username (classic failure signature)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extract creds before @ and after scheme
+      # e.g. 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 "Password appears prefixed by username"; exit 1 } }
+  ' "$app_yml"
+ }
+
   ##############################################
   # Pure-Bash URL encoder for SMTP credentials #
   ##############################################
@@ -989,17 +1113,10 @@
   # 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"
+  # 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"
-}
+  # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
+}

Warum dies Ihre Einschränkung erfüllt
• Kein Ruby/Python auf dem Host: nur Bash + awk + od/printf/grep (alles Standard).
• Keine sed-Ersetzung von Geheimnissen: Wir vermeiden die „Bash → sed → YAML“-Mehrfach-Escape-Falle.
• Atomare Schreibweise: Bearbeitung in eine temporäre Datei, dann mv über containers/app.yml.
• Rückwärtskompatibel: Wenn env: nicht existiert, erstellen wir es minimal; wenn es existiert, aktualisieren wir es vor Ort und entfernen widersprüchliche DISCOURSE_SMTP_* Zeilen.
• Anmeldeinformationen werden vor dem Einfügen URL-kodiert, sodass Sonderzeichen in Benutzer/Passwort den URL-Parser in Rails/Net::SMTP nicht brechen.

Wenn Sie die pro Schlüssel Variablen anstelle von SMTP_URL beibehalten möchten, kann ich einen Geschwister-Patch senden, der (immer noch ohne sed) dieselbe awk-Strategie verwendet, um Folgendes festzulegen:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

…mit YAML-sicheren, doppelt zitierten Werten und verankerten Ersetzungen.

Dann kann ich mir nicht vorstellen, wie das passiert ist. Können Sie es wie in meinem Beispiel reproduzieren?

Haben Sie den gesamten Text in einem einzigen Durchgang kopiert und eingefügt, sodass der Puffer mit der Geschwindigkeit des Einfügens nicht mithalten konnte?

hmmm ich frage mich, ob wir die Logik einfach in launcher2 bündeln können, dann können wir sie verwenden, um das Setup auszuführen.
Die Anzahl der Sprünge, die discourse-setup macht, ist spektakulär.

Ist es möglich, den Launcher in eine binäre ausführbare Datei für die Veröffentlichung zu kompilieren? Auf diese Weise kann der Launcher (Bash) nur die entsprechende ausführbare Datei (Binärdatei) basierend auf dem System herunterladen.

1 „Gefällt mir“