./discourse-setup coloca nome de usuário SMTP no início da senha SMTP

Ambiente

  • Discourse: latest tests-passed
  • Sistema Operacional do Host: [Ubuntu 24.04 LTS / 24.04.3 LTS]
  • Método de instalação: Instalação oficial do Docker, executando ./discourse-setup
  • Plataforma: VPS (Digital Ocean)

Passos para reproduzir

  1. Execute ./discourse-setup em /var/discourse
  2. Insira os detalhes do SMTP quando solicitado:
  1. Conclua a configuração e inspecione containers/app.yml

Resultado esperado

app.yml deve conter dois campos distintos, por exemplo:

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

Resultado real

O campo da senha é escrito com o nome de usuário precedido:

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

Isso causa falha na entrega de e-mails, e o discourse-doctor confirma a senha corrompida.

Observações
• Colocar a senha entre aspas explicitamente no prompt não altera o resultado.
• A senha contém caracteres especiais (@, !), mas o problema não é apenas o escape YAML: a string do nome de usuário é literalmente concatenada no início da senha.
• Reproduzível em várias execuções de ./discourse-setup.

1 curtida

você pode testar se isso se relaciona particularmente a letras na senha, talvez @ / ! revele o bug?

Todo o sed/awk e os scripts bash elaborados em discourse-setup tornam a manutenção bastante complicada. Talvez @pfaffman tenha algumas ideias sobre isso?

1 curtida

Certo. Essa é uma boa aposta.

@Ethsim2, você tem esse problema se mudar a senha para algo com apenas números e letras e que não seja igual a parte do nome de usuário?

Você pode compartilhar os valores que está usando?

Vou tentar dar uma olhada ainda hoje.

Se você removeu a parte 83f…com da senha e deixou apenas a senha (5AH…), ela funciona?

2 curtidas

sim, então eu executo .\launcher rebuild app, já que .\discourse-setup já havia configurado o SWAP

Sim. Esse é o problema. Existem múltiplos níveis de escape que precisam acontecer, como quando o bash lê o valor, quando o bash entrega o valor para o sed, quando o sed o substitui e, então, talvez, quando o arquivo yml o recebe. É um problema conhecido:

Eu recategorizei isso como Support.

Os nomes de usuário SMTP sempre contêm @, não é?

Sim. Não acho que o @ deva causar um problema.

1 curtida

não há ! na minha captura de tela onde ocorre a concatenação

Qual é a senha que você está tentando inserir?

a senha na captura de tela é 5AHQXrf4LDUmRB1J :slightly_smiling_face:

Editar: esta senha é uma senha padrão do Brevo criada em uma conta que foi excluída desde então

Não consigo replicar. Qual sistema operacional você está usando?

root@bro:/var/discourse# ./discourse-setup --skip-connection-test --skip-rebuild
pulando teste de conexão
'samples/standalone.yml' -> 'containers/app.yml'
Encontrados 29GB de memória e 32 núcleos de CPU físicos
definindo db_shared_buffers = 4096MB
definindo UNICORN_WORKERS = 8
parâmetros de memória de containers/app.yml atualizados.

Nome do host para o seu Discourse? [discourse.example.com]: forum.phsics.site

Definindo EC para 2
Pulando verificação de porta.
Endereço de e-mail para contas de administrador? [me@example.com,you@example.com]: jay@literatecomputing.com
Endereço do servidor SMTP? [smtp.example.com]: smtp-relay.brevo.com
Porta SMTP? [587]: 2525
Nome de usuário SMTP? [user@example.com]: 83fca0012@smtp-brevo.com
Senha SMTP? []: 5AHQXrf4LDUmRB1J
Endereço de e-mail de notificação? [noreply@forum.phsics.site]:
Endereço de e-mail opcional para avisos do Let's Encrypt? (ENTER para pular) [me@example.com]:
ID de conta MaxMind opcional (ENTER para continuar sem o banco de dados de geolocalização GeoLite2 MAXMIND) [123456]:

Isso parece certo?

Nome do host          : forum.phsics.site
E-mail             : jay@literatecomputing.com
Endereço SMTP      : smtp-relay.brevo.com
Porta SMTP         : 2525
Nome de usuário SMTP : 83fca0012@smtp-brevo.com
Senha SMTP         : 5AHQXrf4LDUmRB1J
E-mail de notificação: noreply@forum.phsics.site
ID da conta MaxMind: (não definido)
Chave de licença MaxMind: (não definida)

ENTER para continuar, 'n' para tentar novamente, Ctrl+C para sair:
letsencrypt.ssl.template.yml ativado

Arquivo de configuração em containers/app.yml atualizado com sucesso!

Atualizações bem-sucedidas. --skip-rebuild solicitado. Saindo.
root@bro:/var/discourse# grep SMTP containers/app.yml
  ## TODO: O servidor de e-mail SMTP usado para validar novas contas e enviar notificações
  # O ENDEREÇO SMTP é obrigatório
  # AVISO: A senha SMTP deve ser colocada entre aspas para evitar problemas
  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           # (opcional, padrão: true)
  DISCOURSE_SMTP_DOMAIN: discourse.example.com # (obrigatório por alguns provedores)
  #DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: peer        # (opcional, padrão: peer, valores válidos: none, peer, client_once, fail_if_no_peer_cert)
  #DISCOURSE_SMTP_AUTHENTICATION: plain            # (padrão: plain, valores válidos: plain, login, cram_md5)

Digital Ocean ubuntu 24.04

Recomendações geradas por LLM/IA

\nabaixo está um patch conciso que:\n\n- para de usar sed\n- constrói um SMTP_URL com codificação percentual\n- edita containers/app.yml via Ruby’s YAML (Psych), então o escape/codificação YAML é tratado por um analisador real\n- exclui as variáveis SMTP por chave para evitar contradições\n\naplicar com git apply -p0 no repositório discourse_docker.\n\n- - -\n\nPatch 1 -\n\ndiscourse-setup\n\n(escreve SMTP usando Ruby YAML, não 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+ # Constrói um SMTP_URL codificado em URL usando a biblioteca padrão do Python (sem jogos de escape do 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+ # IMPORTANTE: lê variáveis sem manipulação de barra invertida\n+ # (estas vêm de prompts anteriores; apenas garanta que -r seja usado no momento do prompt)\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+ # Usa Ruby para carregar/modificar/descarregar YAML com segurança (elimina 3 camadas de escape)\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+# Garante que a estrutura de nível superior seja um Hash e tenha env\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml não analisa para um Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Escreve SMTP_URL de linha única; remove variáveis por chave para evitar conflitos\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+# Descarrega de volta. (Psych preserva strings com aspas seguras conforme necessário.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # rápida verificação de sanidade para a falha clássica de \"senha prefixada pelo nome de usuário\"\n+ python3 - \u003c\u003c'PY'\n+import re, sys\n+y = open(\"containers/app.yml\",\"r\",encoding=\"utf-8\").read()\nm = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)\n+assert m, \"SMTP_URL ausente após a escrita\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"Credenciais SMTP_URL ausentes ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Senha parece prefixada pelo nome de usuário\"\n+print(\"SMTP_URL parece razoável.\")\n+PY\n+}\n+\n- # Escreve entradas SMTP por chave (endereço/porta/nome de usuário/senha)\n- # (legado: realizado via substituições de sed)\n- # NOTA: historicamente frágil com caracteres especiais\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+ # (escritas legadas por chave removidas em favor de SMTP_URL via YAML)\n+}\n\n\nentão Patch 2 -\n\ntemplates/web.template.yml\n\n(para documentar o caminho mais seguro)\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+ ## Configuração SMTP preferida em linha única (definida por discourse-setup):\n+ ## Codifica em URL o nome de usuário e a senha; exemplo:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Se você não puder usar SMTP_URL, pode definir variáveis por chave em vez disso.\n ## Cuidado ao editar essas linhas com ferramentas de shell, pois os valores podem incluir\n ## caracteres como @, :, /, \", \\ ou novas linhas.\n\n\npor que isso funciona (e o que evita)\n\t•\tcamada bash: interpolamos apenas variáveis simples; segredos são passados para Python/Ruby via stdin/argv, não através de regexes sed ou avaliações de shell.\n\t•\tcamada sed: removida completamente.\n\t•\tcamada YAML: Ruby/Psych lida com aspas e escape corretamente; sem aspas manuais.\n\t•\tcredenciais SMTP: o escape % na SMTP_URL é o lugar certo para codificar caracteres especiais para autenticação.\n\nse você preferir manter variáveis por chave, posso fornecer um patch irmão que usa a mesma abordagem Ruby-YAML para definir DISCOURSE_SMTP_* diretamente (ainda sem sed), mas a rota SMTP_URL é a mais limpa porque é uma chave, uma escrita, uma etapa de codificação.\n

Infelizmente, não podemos depender da instalação do Ruby no sistema host em instalações do discourse - seria relativamente simples ter o script inteiro em Ruby se pudéssemos, mas não há garantia.

2 curtidas
também gerado por LLM/IA

Entendido — sem Ruby/Python no host. Aqui está um patch puramente Bash + awk que:
• constrói um SMTP_URL codificado em percentual usando apenas Bash (loop byte a byte; sem Python),
• insere ou substitui SMTP_URL sob o bloco env: usando awk (sem sed),
• remove as linhas DISCOURSE_SMTP_* por chave (exclusões ancoradas seguras),
• adiciona uma pequena verificação de sanidade usando apenas grep/awk.

Aplique com git apply -p0 no repositório 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?"

+  ##############################################
+  # Codificador de URL puro Bash para credenciais SMTP #
+  ##############################################
+  # Codifica tudo, exceto A-Z a-z 0-9 . _ ~ -
+  # Funciona byte a byte; requer bash e printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # define a localidade C para obter semântica de byte
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Obtém o valor do byte: imprime o caractere, lê com od, em seguida formata %HH
+          # Evita dependências pesadas externas; od está em coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # Se od falhou de alguma forma (vazio), retorne ao hexadecimal via printf %02X do primeiro 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"
+  }
+
+  # Constrói SMTP_URL (linha única) a partir das respostas coletadas
+  # Estas variáveis são coletadas anteriormente em 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}"
+
+  ########################################################
+  # Edição segura para YAML via awk (sem sed / sem runtimes externos)
+  # - garante que env: exista
+  # - insere ou substitui SMTP_URL sob env:
+  # - remove chaves DISCOURSE_SMTP_*
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # detecta a linha env: no nível superior (início da linha, possivelmente com indentação 0..)
+    # consideraremos indentação de dois espaços para os filhos.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # saindo do bloco env: quando a indentação retorna a 0 ou à próxima chave de nível superior
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # enquanto estiver em env:, lida com substituições e exclusões
+    in_env {
+      # descarta linhas DISCOURSE_SMTP_* por chave inteiramente
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # substitui a linha SMTP_URL existente
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # Se env: nunca existiu, anexe-o com a chave
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+      } else if (in_env && !inserted) {
+        # env: existiu e ainda estávamos nele no final do arquivo
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Verificação de sanidade: guarda básica contra corrupção #
+  ##############################################
+  # 1) SMTP_URL presente
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL not written to $app_yml"
+  # 2) senha não prefixada pelo nome de usuário (assinatura clássica de falha)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extrai credenciais antes de @ e após o esquema
+      # ex: 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 #
   ##############################################
@@ -960,16 +1084,4 @@
   # (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"
-}
-
-- # 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.
+}

Por que isso atende à sua restrição
• Zero Ruby/Python no host: apenas Bash + awk + od/printf/grep (todos os padrões).
• Sem substituição de segredos por sed: evitamos a armadilha de “Bash → sed → YAML” com múltiplos escapes.
• Escrita atômica (aproximadamente): edita em um arquivo temporário, depois move sobre containers/app.yml.
• Seguro para versões anteriores: se env: não existir, nós o criamos minimamente; se existir, atualizamos no local e removemos linhas conflitantes DISCOURSE_SMTP_*.
• As credenciais são codificadas em URL antes da inserção, portanto, caracteres especiais no usuário/senha não quebrarão o analisador de URL em Rails/Net::SMTP.

Se você preferir manter as variáveis por chave em vez de SMTP_URL, posso enviar um patch irmão que (ainda sem sed) usa a mesma estratégia awk para definir:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

… com valores entre aspas duplas seguros para YAML e substituições ancoradas.

Então não consigo imaginar como isso aconteceu. Você consegue replicar como no meu exemplo?

Você copiou e colou todo o texto em um único lote para que o buffer não conseguisse acompanhar a velocidade com que você estava colando?

hmmm eu me pergunto se podemos simplesmente agrupar a lógica no launcher2, então podemos usá-lo para executar a configuração.
A quantidade de saltos que o discourse-setup está dando é espetacular.

É possível compilar o launcher em um arquivo executável binário para lançamento? Dessa forma, o launcher (bash) poderá baixar apenas o arquivo executável (binário) correspondente com base no sistema.

1 curtida