Skip to content
近期更新

6 週踩雷集錦,那些卡了我半天的小事

debuggingphpbbdockermysqlblog-series

6 週踩雷集錦,那些卡了我半天的小事

1Password SSH agent 撞 MaxAuthTries=3、phpBB 顯示成功訊息但根本沒寄信、sed 解析 .env 密碼含等號被截斷、Prettier 把 phpBB 模板搞壞,每個都是 1 到 3 小時 debug 的小坑,集合起來才是真實的 6 週體感。系列文 5 篇之 #5,最終回。

為什麼要記 debug 過程

#4 那篇寫完了部署架構。整個 phpbb-restore 系列的「主軸」基本就那四篇,這篇是收尾,記錄前面文章因為篇幅與主題沒有展開的那些小事。

寫 debug 過程通常被認為瑣碎、沒人想看,但實務上這些小坑加起來的時間佔了整個專案的一大半。如果只看完成的成品,會以為這 6 週是順順跑下來的;事實上每天晚上有一半時間在 debug 看似不該存在的問題,看到一個錯誤訊息要花 1 到 3 小時才能挖到根因。把它們記下來有兩個好處,一是公開讓踩過同樣坑的人少花一輪時間 Google,二是給自己未來重看當作 debug pattern 的對照。

下面 8 個是我這次印象最深的,每個都吃掉至少一個晚上。

1Password SSH agent 撞 Bluehost MaxAuthTries=3

ssh bluehost 第一次連回去就拋 Too many authentication failures 拒接,連密碼提示都沒看到。這個錯誤訊息字面看是「認證失敗太多次」,但我密碼還沒輸入過、key 也還沒嘗試過,怎麼就太多次?

挖了一陣子才發現,1Password SSH agent 一次會拋出我整本 1Password 裡的 6 把 SSH key(91APP GitLab、DigitalOcean、GitHub、id_ed25519、Mac mini 2012、Bluehost),順序是按 1Password 內部的儲存順序,Bluehost 那把排在第 6。OpenSSH 的 default 行為是「每次連線最多嘗試 6 把 key 然後放棄」,但伺服器端可以設 MaxAuthTries,Bluehost 設的是 3。也就是 ssh client 試了前 3 把(都不是 Bluehost 那把)就被 server 踢掉、第 4 到 6 把根本沒機會試。

修法是 ~/.ssh/config 給 Bluehost 加 IdentitiesOnly yesIdentityFile ~/.ssh/bluehost.pub,明確告訴 ssh 只試這把 key、不要丟整本給 server。public key 從 1Password 桌面 app 的 Bluehost 項目 Public Key 欄位複製、pbpaste > ~/.ssh/bluehost.pubchmod 600 即可。設定完一次就通了。

教訓是 1Password SSH agent 的便利有它的副作用,agent 把所有 key 都丟進連線會撞 server 端 throttle。對這種有 hard limit 的 server 一定要 pin 特定 key。

phpBB 顯示「忘記密碼信已寄出」但根本沒寄

debug 過程最戲劇的就是這個。我在 phpBB 上點忘記密碼、輸入 email、頁面顯示「reset email 已寄出,請查收」,但 Gmail 永遠收不到。我以為是 SMTP 或 DNS 問題,把 Resend 配置、SPF、DKIM、port 全 trace 一遍都正確。

最後在 phpBB 原始碼挖出真相。reset_password.php 在第 227 行附近做了一個 anti-enumeration 的設計,如果使用者已經有一個 reset_token 還沒過期(24 小時內申請過第二次),就 silently skip 寄信、但顯示成功訊息。理由是防止攻擊者用「點忘記密碼看反應」來判斷 email 是否註冊在系統裡。從安全角度合理,從 debug 角度地獄。

修法是 SQL 直接清掉 token:UPDATE phpbb3_users SET reset_token='', reset_token_expiration=0 WHERE username='<my_username>';,再點一次忘記密碼、收信、確認流程通了。

這個雷有兩個 takeaway。第一是 phpBB 顯示成功訊息不等於真寄信,所有「使用者體驗成功」的回饋對 debug 沒意義,要看寄信端的 log。第二是讀過 phpBB 原始碼之後我才發現這類 anti-enumeration 設計散落在 phpBB 的 forgot password、registration、login 各處,每一處都會有「看似成功但其實 silent skip」的可能。對 debug 系統信流程來講要連 source code 一起讀。

sed 解析 .env 撞到密碼含等號

VPS 上的 cron 要從 /opt/phpbb/.env 抽出 MySQL 密碼塞進 mysqldump -p$MYSQL_PW。第一版 cron 寫法用 cut -d= -f2,意思是用 = 切 line 取第 2 欄。看起來合理,跑下去 mysqldump 偶爾會 401。

問題在於 1Password 產的 32 字元 alphanumeric 密碼有時候會包含 =,比如類似 MYSQL_ROOT_PASSWORD=AbCdEf=GhIj=... 的 pattern(這個是示意、不是實際密碼)。cut -d= -f2 拿到的是第一個 = 跟第二個 = 之間的字串(中間那一段),密碼被截斷成只剩前半段。mysqldump 用截斷的密碼登入當然 401。

修法改成 sed -n 's/^MYSQL_ROOT_PASSWORD=//p',意思是「找以 MYSQL_ROOT_PASSWORD= 開頭的行、把這個前綴拿掉、回印剩下的整段」。這樣不管密碼裡有幾個 = 都能完整抽出來。

教訓是 .env 解析不要用 cut,用 sed 或 awk 加明確 prefix match。如果一定要用 cut,密碼產生規則要排除 = 跟其他 special char。

phpBB 3.3.15 加 PHP 8 加 en lang 觸發 DATETIME_FORMAT warning

某個 user 設定了 user_lang=en 之後登入訪問 ucp,瀏覽器頁面出現幾百行 [phpBB Debug] PHP Warning: Undefined array key DATETIME_FORMAT,整個介面被 warning 訊息淹沒。同樣的 user 切回 zh_cmn_hant 又正常了。

這個 warning 不是 lang pack 缺檔。我把官方 phpBB 3.3.15 的 language/en/common.php 跟我們站上的逐行 diff,完全相同。問題在於 phpBB 自己對 PHP 8 的相容性有一個 timing bug,DATETIME_FORMAT 在 lang load 過程中某個瞬間還沒被定義就被讀取,PHP 8 的 strict array key 檢查會噴 warning。我們的版本踩到這個 bug,但 user_lang=zh_cmn_hant 不會觸發、只有 user_lang=en 會。

修法是不去改 phpBB core(會被下次升級覆蓋),而是 patch includes/startup.php 第 23 行,把 error_reporting()$level 加上 & ~E_WARNING 抑制 warning 的 echo。實際的 warning 還會寫到 php_errors.log、不會無聲消失,只是 user 視覺上不會看到。

教訓是 phpBB 3.3 對 PHP 8 的相容性其實還有不少 cosmetic bug,這類 fix 應該集中放在 startup level、未來升級時拿掉。

MySQL init-db/ 多 DB 陷阱

docker-compose.yml 我用了 MySQL image 的 docker-entrypoint-initdb.d/ 機制,把 phpBB 兩站的 dump 放進這個目錄,希望 container 第一次啟動時自動匯入。eq2 站正常起來,eq 站論壇開出來 500 error。

挖了一個多小時才找到根因。docker-entrypoint-initdb.d/ 的 init 腳本是這樣跑的,先 create environment 變數 MYSQL_DATABASE 指定的單一 schema、然後依字母順序執行 init 目錄下所有 .sql 檔在那個單一 schema 裡。問題是我的兩份 dump 是 mysqldump --databases eq2_dbmysqldump --databases eq_db 出來的,但實際 dump 內容沒寫 CREATE DATABASEUSE(mysqldump 預設行為),所有 SQL 都被灌進 MYSQL_DATABASE 指定的那一個 schema。eq 站的表全跑進 eq2_db 裡,自己的 schema eq_db 從來沒被建立過。

修法兩個。第一是新增一個 00-create-eq-db.sqlCREATE DATABASE IF NOT EXISTS eq_db;,第二是手動在兩份 dump 開頭補 USE <dbname>; 確保 SQL 進到對的 schema。重新 init(要先 docker compose down -v 砍 volume),兩站都正常。

教訓是 docker-entrypoint-initdb.d/ 對多 DB 場景不友善,預設行為假設你只有一個 schema。要塞多個 schema 必須手動在每份 dump 補 CREATE DATABASEUSE

Calendar extension 缺一個 en 路徑就 500

Phase 4 寫完 Calendar 唯讀 extension 上線之後,ACP 進到「版面權限 / 群組權限 / 管理員權限」5 個連結會 PHP fatal 500。其他 ACP 頁正常、論壇前台也正常,只有那 5 個權限連結。

stack trace 顯示 acp_permissions::main() 呼叫到 add_permission_language()language_file_loader.php:186 找不到檔。挖原始碼發現它要載入 language/<user_lang>/acp/permissions_calendar.php,找不到就 fallback 到 language/en/acp/permissions_calendar.php,再找不到就 fatal。

問題出在這個 extension 是 2009 年某個 phpBB MOD 作者寫的,他用 en_us 作為英文目錄名(phpBB MOD 時代習慣),於是檔案路徑是 language/en_us/acp/permissions_calendar.php。phpBB 3.3 的 fallback 順序找 <user_lang>en,從來不會找 en_us。對 user_lang=zh_cmn_hant 的人來說,找不到 zh 版本後 fallback 到 en,en 也沒有,fatal 500。

修法是把 permissions_calendar.phpen_us/acp/ 複製到 en/acp/zh_cmn_hant/acp/,再砍掉 cache/production/data_ext_finder.php 強制 phpBB 重建 extension cache。線上跟本機 template 兩邊都同步了一份。

教訓是接手老 extension 時,目錄命名假設要重新驗證。en_us vs en 這類細節 phpBB 2 時代各家 MOD 都不一致,到 phpBB 3.3 標準化成 en

sunmoon/pages 擴充功能,資料庫說啟用、後台說壞掉

這個坑是做別的事的時候意外撞到的。我為了驗證 phpBB 的 DB 帳號權限進了 ACP 的「擴充功能管理」,看到三個自製 extension 裡的 sunmoon/pages 被列在「停用的擴充功能」段,旁邊一行錯誤「必填欄位 version 沒有設定」。但奇怪的是,這個 extension 提供的三個頁面——關於我們、下載、DKP 退役說明——前台打開全都正常 200。

我先查資料庫。phpBB 記錄 extension 狀態的 phpbb3_ext 表裡,sunmoon/pagesext_active 是 1,也就是資料層認為它是啟用的。但 ACP 後台說它停用。兩邊打架。接著對比三個自製 extension 的 composer.jsonapplicationformcalendar 都有 "version" 欄位,只有 pages 漏了。phpBB 3.3 載入 extension metadata 時把 version 列為必填,缺了就判定整個 extension invalid、ACP 標成停用加錯誤訊息。那為什麼前台三個頁面還活著?因為頁面的 route 註冊資訊還在 phpBB 的 cache 裡,cache 沒失效之前 route 照樣 work。

修法是 composer.json 補一行 "version": "1.0.0" 跟另外兩個 extension 對齊,再砍掉 cache/production/data_ext_finder.php 強制 phpBB 重建 extension metadata cache。重整 ACP,sunmoon/pages 跳回「啟用的擴充功能」段。

教訓是這是一個 dormant bug,站台表面一直好好的,但它其實已經壞了——只是靠 cache 撐著,下一次任何原因清了 cache,三個頁面就會一起 404。「資料庫說啟用、後台說停用」這個狀態不一致就是它的線索。從 phpBB 3.0 的 MOD 體系搬到 3.3 的 extension 體系時,composer.json 的必填欄位很容易漏,applicationformcalendar 剛好沒漏、pages 漏了,純粹是手工搬移時的疏忽。

Prettier 把 phpBB 模板靜悄悄改壞

我習慣用 VS Code 加 Prettier 自動格式化,這個習慣在 phpbb-restore 中段差點把整個論壇模板搞壞。

phpBB 的 .html 模板用一套自家的 control flow 語法,像 <!-- IF S_USER_LOGGED_IN --> 這類 directive 會被 phpBB 模板引擎處理。Prettier 把 .html 當 standard HTML 格式化,遇到長 directive 會自動 wrap 換行:

<!-- IF S_USER_LOGGED_IN
  AND S_DISPLAY_BANNER -->

phpBB 模板引擎不認得跨行的 directive,編譯時 silently 略過。後果是模板看起來都還在、phpBB 不會報錯,但某些 conditional render 的元素整片消失。我有一次更新一個樣式檔,存檔自動格式化跑了一輪,過了 20 分鐘才注意到 navbar 的某個按鈕從前台消失了。

修法是用 Python 腳本而不是 Edit 工具改模板,腳本只動目標 byte range、不重新 format 整檔。後來又加了 Prettier 的 .prettierignore 把 phpBB 模板路徑全 exclude 掉。

教訓是 phpBB 的模板語法跟標準 HTML 不兼容,自動格式化工具預設會破壞它,要麼明確 ignore 要麼換工具。

系列收尾

5 篇寫完了。

#1 講為什麼救這個 23 年的舊論壇,是動機篇。#2 講一個 PM 怎麼用 Claude Code 自己做這件事,包括能委託什麼、必須親自做什麼,以及這個專案怎麼意外長出 /seer skill。#3 講資料復原的多代升級鏈、中文編碼、附件對齊驗證。#4 講部署架構、Traefik 一個 domain 分流到三站、SMTP 怎麼繞過 DigitalOcean 封 port。這篇 #5 集中收錄 8 個吃時間的小坑。

對抗遺忘是一種勞動,整個過程不只把論壇救活,還順手把 23 年公會的紀錄重新放在自己掌控之下。如果這個系列文對其他想保存自己社群數位文物的人有幫助,那 33 天的時間跨度就是值得的。

論壇現在站在 eq2.3d-olg.net/communityeq.3d-olg.net,The Sun Moon Guild 在 EverQuest II Halls of Fate 伺服器持續運作中。歡迎老友回來、歡迎陌生人路過。


系列文 5 篇之 #5,phpBB Restore Series 完。

留言討論

以 VitePress 建置