./discourse-setup يضع اسم مستخدم SMTP في بداية كلمة مرور SMTP

البيئة

  • Discourse: أحدث الاختبارات-اجتازت
  • نظام تشغيل المضيف: [Ubuntu 24.04 LTS / 24.04.3 LTS]
  • طريقة التثبيت: تثبيت Docker الرسمي، تشغيل ./discourse-setup
  • المنصة: VPS (Digital Ocean)

خطوات التكرار

  1. قم بتشغيل ./discourse-setup من /var/discourse

  2. أدخل تفاصيل SMTP عند المطالبة:

  3. أكمل الإعداد وافحص الحاويات/app.yml

النتيجة المتوقعة

يجب أن يحتوي app.yml على حقلين مميزين، على سبيل المثال:

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

النتيجة الفعلية

يتم كتابة حقل كلمة المرور مع إضافة اسم المستخدم في البداية:

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

يؤدي هذا إلى فشل تسليم البريد، ويؤكد discourse-doctor كلمة المرور المشوهة.

ملاحظات

  • الاقتباس الصريح لكلمة المرور عند المطالبة لا يغير النتيجة.
  • تحتوي كلمة المرور على أحرف خاصة (@، !)، ولكن المشكلة ليست مجرد اقتباس YAML: يتم دمج سلسلة اسم المستخدم حرفيًا في بداية كلمة المرور.
  • يمكن تكرارها عبر عمليات تشغيل متعددة لـ ./discourse-setup.
إعجاب واحد (1)

هل يمكنك اختبار ما إذا كان هذا يتعلق بأحرف معينة في كلمة المرور ربما @ / ! يكشف عن الخطأ؟

كل نصوص sed/awk والبرمجة النصية المتقدمة في discourse-setup تجعل صيانتها صعبة للغاية. ربما لدى @pfaffman بعض الأفكار هنا؟

إعجاب واحد (1)

حسناً. هذا رهان جيد.

@Ethsim2 إذا قمت بتغيير كلمة المرور إلى شيء يحتوي على أرقام وأحرف فقط وليس نفس جزء من اسم المستخدم، فهل تواجه هذه المشكلة؟

هل يمكنك مشاركة القيم التي تستخدمها؟

سأحاول إلقاء نظرة لاحقًا اليوم.

إذا قمت بإزالة الجزء 83f…com من كلمة المرور وتركت كلمة المرور فقط (5AH…)، فهل تعمل؟

إعجابَين (2)

نعم، ثم أقوم بتشغيل .\launcher rebuild app، بما أن .\discourse-setup قام بالفعل بتكوين SWAP

نعم. هذه هي المشكلة. هناك مستويات متعددة من الهروب التي تحتاج إلى حدوث، مثل عندما تقرأ bash القيمة، وعندما تسلم bash القيمة إلى sed، وعندما يستبدلها sed، وبعد ذلك، ربما، عندما تحصل عليها ملف yml. إنها مشكلة معروفة:

لقد أعدت تصنيف هذا إلى Support.

أسماء مستخدمي SMTP تحتوي دائمًا على @، أليس كذلك؟

لا أعتقد أن @ يجب أن تسبب مشكلة.

إعجاب واحد (1)

لا يوجد علامة تعجب ! في لقطة الشاشة الخاصة بي حيث يحدث الدمج

ما هي كلمة المرور التي تحاول إدخالها؟

كلمة المرور الموجودة في لقطة الشاشة هي 5AHQXrf4LDUmRB1J :slightly_smiling_face:

تعديل: كلمة المرور هذه هي كلمة مرور افتراضية لـ Brevo تم إنشاؤها في حساب تم حذفه منذ ذلك الحين

لا يمكنني تكرار ذلك. ما هو نظام التشغيل الذي تستخدمه؟

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)

Digital Ocean أوبونتو 24.04

توصيات تم إنشاؤها بواسطة LLM/AI

\nفيما يلي تصحيح دقيق يقوم بما يلي:\n\n- يتوقف عن استخدام sed\n- ينشئ عنوان URL مشفر بنسبة SMTP_URL\n- يقوم بتحرير containers/app.yml باستخدام Ruby’s YAML (Psych)، لذا يتم التعامل مع الاقتباس/الهروب من YAML بواسطة محلل حقيقي\n- يحذف متغيرات SMTP لكل مفتاح لتجنب التناقضات\n\nقم بالتطبيق باستخدام git apply -p0 في مستودع discourse_docker.\n\n- - -\n\nالتصحيح 1 -\n\ndiscourse-setup\n\n(كتابة SMTP باستخدام Ruby YAML، وليس 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+ # بناء عنوان URL مشفر بنسبة SMTP_URL باستخدام مكتبة Python القياسية (لا توجد ألعاب هروب من 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+ # هام: قراءة المتغيرات بدون تحريف الشرطة المائلة العكسية\n+ # (تأتي هذه من مطالبات سابقة؛ فقط تأكد من استخدام -r في وقت المطالبة)\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+ # استخدام Ruby لتحميل/تعديل/إخراج YAML بأمان (يقضي على 3 طبقات من الهروب)\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+# التأكد من أن الهيكل العلوي هو Hash ويحتوي على env\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml does not parse to a Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# كتابة SMTP_URL في سطر واحد؛ حذف المتغيرات لكل مفتاح لتجنب التعارضات\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+# إخراج مرة أخرى. (Psych يحافظ على السلاسل النصية المقتبسة بأمان حسب الحاجة.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # فحص سريع للتأكد من فشل \"كلمة المرور مسبوقة باسم المستخدم\" الكلاسيكي\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 missing after write\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"SMTP_URL creds missing ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Password appears prefixed by username\"\n+print(\"SMTP_URL looks sane.\")\n+PY\n+}\n+\n- # كتابة إدخالات SMTP لكل مفتاح (العنوان/المنفذ/اسم المستخدم/كلمة المرور)\n- # (قديم: تم تنفيذه عبر استبدالات sed)\n- # ملاحظة: تاريخياً هش مع الأحرف الخاصة\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+ # (تمت إزالة كتابات المفاتيح القديمة لصالح SMTP_URL عبر YAML)\n+}\n\n\nثم التصحيح 2 -\n\ntemplates/web.template.yml\n\n(لتوثيق المسار الأكثر أمانًا)\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+ ## التكوين المفضل لـ SMTP في سطر واحد (تم تعيينه بواسطة discourse-setup):\n+ ## تشفير اسم المستخدم وكلمة المرور بنسبة مئوية؛ مثال:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## إذا لم تتمكن من استخدام SMTP_URL، يمكنك تعيين متغيرات لكل مفتاح بدلاً من ذلك.\n ## احذر من أن تعديل تلك الأسطر بأدوات shell يمكن أن يكون هشًا إذا كانت القيم تتضمن\n ## أحرفًا مثل @، :، /، \"، \\، أو أسطر جديدة.\n\n\nلماذا يعمل هذا (وما الذي يتجنبه)\n\t•\tطبقة bash: نقوم فقط بتضمين متغيرات بسيطة؛ يتم تمرير الأسرار إلى Python/Ruby عبر stdin/argv، وليس من خلال تعبيرات sed العادية أو تقييمات shell.\n\t•\tطبقة sed: تمت إزالتها بالكامل.\n\t•\tطبقة YAML: يتعامل Ruby/Psych مع الاقتباس والهروب بشكل صحيح؛ لا يوجد اقتباس يدوي.\n\t•\tبيانات اعتماد SMTP: تشفير النسبة المئوية في SMTP_URL هو المكان المناسب لتشفير الأحرف الخاصة للمصادقة.\n\nإذا كنت تفضل الاحتفاظ بالمتغيرات لكل مفتاح، يمكنني أن أقدم لك تصحيحًا شقيقًا يستخدم نفس نهج Ruby-YAML لتعيين DISCOURSE_SMTP_* مباشرة (لا يزال بدون sed)، ولكن مسار SMTP_URL هو الأكثر نظافة لأنه مفتاح واحد، كتابة واحدة، خطوة تشفير واحدة.\n

للأسف لا يمكننا الاعتماد على تثبيت Ruby على النظام المضيف في تثبيتات discourse - سيكون من السهل نسبيًا جعل البرنامج النصي بأكمله برنامج Ruby إذا كان بإمكاننا ذلك، ولكن لا يوجد ضمان.

إعجابَين (2)
تم إنشاؤه بواسطة LLM/AI أيضًا

حسنًا - لا يوجد Ruby/Python على المضيف. إليك تصحيح Bash + awk نقي يقوم بما يلي:
• يبني SMTP_URL مشفرًا بالنسبة المئوية باستخدام Bash فقط (حلقة بايت بايت؛ لا يوجد Python)،
• يدرج أو يستبدل SMTP_URL تحت كتلة env: باستخدام awk (لا يوجد sed)،
• يزيل أسطر DISCOURSE_SMTP_* لكل مفتاح (عمليات حذف آمنة مثبتة)،
• يضيف فحصًا بسيطًا للتحقق من الصحة باستخدام grep/awk فقط.

قم بالتطبيق باستخدام git apply -p0 في مستودع 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 "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 #
   ##############################################
@@ -980,16 +1104,4 @@
   # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
 }

Why this meets your constraint
• Zero Ruby/Python on the host: only Bash + awk + od/printf/grep (all standard).
• No sed replacement of secrets: we avoid the “Bash → sed → YAML” multi-escape trap.
• Atomic-ish write: edit into a temp file, then mv over containers/app.yml.
• Backwards-safe: if env: doesn’t exist, we create it minimally; if it does, we update in place and remove conflicting DISCOURSE_SMTP_* lines.
• Credentials are URL-encoded before insertion, so special chars in user/pass won’t break the URL parser in Rails/Net::SMTP.

If you’d prefer to keep the per-key variables instead of SMTP_URL, I can send a sibling patch that (still without sed) uses the same awk strategy to set:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

…with YAML-safe double-quoted values and anchored replacements.

إذًا لا يمكنني تخيل كيف حدث ذلك. هل يمكنك تكراره كما في مثالي؟

هل نسخت/لصقت كل النص في دفعة واحدة بحيث لم يتمكن المخزن المؤقت من مواكبة سرعة لصقك؟

هممم، أتساءل عما إذا كان بإمكاننا تجميع المنطق في launcher2 ثم يمكننا استخدامه لتشغيل الإعداد.
كمية الحلقات التي يقفز فوقها discourse-setup مذهلة.

هل من الممكن تجميع المشغل في ملف تنفيذي ثنائي للإصدار؟ بهذه الطريقة، يمكن للمشغل (bash) فقط تنزيل الملف التنفيذي (الثنائي) المقابل بناءً على النظام.

إعجاب واحد (1)