留言板架构记录
纯文本存储 + CGI 处理
整体流程
留言提交后的完整路径如下:
[访客填写表单] → [CGI 脚本接收] → [写入 guestbook.txt]
↓
[触发 rebuild-all] → [build.ps1 读取数据] → [注入侧边栏 HTML]
↓
[部署新页面] → [访客看到留言]
留言后不会实时出现,而是在下一次构建时被编译进静态页面里(感觉有必要做个实时更新)[2]。
1. 前端表单
表单直接写在 sidebar-left.html 里,位于左侧栏留言板区域。一个标准的 HTML 3.2 表单:
<form action="/cgi-bin/guestbook.py" method="post">
Name: <input type="text" name="name">
Email: <input type="text" name="email" placeholder="Optional">
内容: <textarea name="content"></textarea>
[x] 显示IP <input type="checkbox" name="show_ip" value="yes">
<input type="submit" value="发送留言">
</form>
四个字段:name(必填)、email(可选,填写后用户名会变成 mailto 链接)、content(必填)、show_ip(控制是否公开展示 IP),没有验证码(暂时不考虑)
2. CGI 后端
触发留言后表单提交到 /cgi-bin/guestbook.py CGI[1] 脚本处理。
输入处理
def parse_form_data():
method = os.environ.get('REQUEST_METHOD', 'GET')
if method == 'GET':
qs = os.environ.get('QUERY_STRING', '')
form_data = parse_qs(qs)
else:
length = int(os.environ.get('CONTENT_LENGTH', 0))
body = sys.stdin.read(length)
form_data = parse_qs(body)
return {k: v[0] for k, v in form_data.items()}
CGI 的数据来源非常原始:GET 请求从 QUERY_STRING 环境变量读,POST 请求从标准输入读。脚本会解析 URL-encoded 表单数据,然后进行清理:
def sanitize(text):
text = text.replace('\n', ' ').replace('\r', ' ') # 移除换行
text = text.replace('|', ' ') # 移除分隔符
return html.escape(text) # 转义 HTML
移除 | 是因为它被用作数据文件的分隔符;移除换行防止数据格式被破坏;HTML 转义防止 XSS 攻击。
3. 客户端 IP 检测
因为部署在反向代理后面,直接读 REMOTE_ADDR 拿到的只会是代理服务器的 IP。所以 web_server.py 在每次请求时都会解析真实 IP,并通过环境变量传给 CGI:
# 在 CustomHandler._inject_real_ip() 中
cf_ip = headers.get('CF-Connecting-IP') # Cloudflare
xff = headers.get('X-Forwarded-For') # 标准代理头
real_ip = headers.get('X-Real-IP') # Nginx 常用
if cf_ip:
real_client_ip = cf_ip
elif xff:
real_client_ip = xff.split(',')[0].strip() # 取第一个
elif real_ip:
real_client_ip = real_ip
else:
real_client_ip = client_ip # 直连 fallback
os.environ["REAL_CLIENT_IP"] = real_client_ip # 注入 CGI 环境
优先级:Cloudflare > X-Forwarded-For > X-Real-IP > 直连。CGI 脚本通过 os.environ.get("REAL_CLIENT_IP") 拿到正确的客户端 IP。
4. 数据存储
留言存储在 data/runtime/guestbook.txt,每行一条,字段用 | 分隔:
name|email|content|ip|time|show_ip ← 新格式(6字段)
name|content|ip|time|show_ip ← 旧格式(5字段,兼容)
示例:
DragonRSTER|dragonrster@foxmail.com|嘿,现在支持填写邮箱了|hidden|2026-04-26 18:52:27|no
xintai||This message was sent from win98|180.154.121.226|2026-04-24 23:33:41|yes
如果用户选择不显示 IP,IP 字段会写入 hidden 而不是真实地址。整个文件就是一个纯文本 CSV变体,可以用任何工具查看。目前积累了 30 多条留言,最早一条来自 2020年(上版博客的遗物)。
5. 构建时注入
每次执行 rebuild-all.ps1 时,build.ps1 会进行以下操作:
# 读取 guestbook.txt
$lines = Get-Content $guestbookFile -Encoding UTF8
# 取最后 20 条留言,反转顺序(最新在上)
$lastLines = $lines | Select-Object -Last 20
[array]::Reverse($lastLines)
# 逐条生成 HTML
foreach ($line in $lastLines) {
$parts = $line -split '\|'
# 兼容新旧两种格式...
# 生成: 姓名(带 mailto) + 内容 + IP(可选) + 时间
}
# 注入到 sidebar-left.html 的占位符位置
$sidebarLeft = $sidebarLeft -replace "<!-- GUESTBOOK_MESSAGES -->", $messagesHtml
留言内容被直接编译进 HTML,写在 sidebar-left.html 的 <!-- GUESTBOOK_MESSAGES --> 占位符处。显示逻辑:
有 email → 用户名渲染为 <a href="mailto:..."> 链接
show_ip 为 yes → 在留言下方显示 IP 地址(灰色小字)
所有留言包在一个滚动容器内,最多显示 20 条
因为 guestbook.txt 现在支持 email 字段,所以新旧格式会同时兼容,读取时根据字段数量自动判断。
6. 服务器端
web_server.py 继承自 Python 标准库的 CGIHTTPRequestHandler,在标准 CGI 支持之上加了几层自定义逻辑:
路径映射:/ → index.html,/blog-<em> → dist/,/assets/</em> → dist/assets/
安全保护:/data/、/scripts/、/src/ 直接返回 403
CGI 目录:/cgi-bin/ 使用标准 CGI 处理流程
日志记录:每次请求写入 data/logs/YYYY-MM-DD.log,旧日志自动 gzip 压缩
启动方式:
python web_server.py
# 监听 0.0.0.0:81,默认端口 81
7. 一些防御措施
虽然纯人力,但还是做了一些基础限制:
字段清理:移除 | 和换行符,防止数据格式注入
HTML 转义:html.escape() 处理所有用户输入,防止 XSS
IP 可控:用户可以选择不公开 IP,写入 hidden 而非真实地址
目录保护:/data/、/scripts/、/src/ 从 HTTP 层直接 403
robots.txt:禁止爬虫访问 /cgi-bin/ 和 /data/
| [1] | CGI(Common Gateway Interface)诞生于 1993 年,由 NCSA 的 Rob McCool 提出,是 Web 最早的动态内容技术标准。虽然每次请求都要 fork 进程,但对于低流量站点来说完全够用,而且不需要任何框架依赖。
|
| [2] | 现在已改成提交留言后后台自动编译
|
|