نعم. هذه هي المشكلة. هناك مستويات متعددة من الهروب التي تحتاج إلى حدوث، مثل عندما تقرأ bash القيمة، وعندما تسلم bash القيمة إلى sed، وعندما يستبدلها sed، وبعد ذلك، ربما، عندما تحصل عليها ملف yml. إنها مشكلة معروفة:
\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 إذا كان بإمكاننا ذلك، ولكن لا يوجد ضمان.
حسنًا - لا يوجد 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: