目录
首页 » 博客 » 留言板架构记录 [切换主题] [English]

留言板架构记录

纯文本存储 + 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]现在已改成提交留言后后台自动编译

    « 2026年4月 · 最近在干些什么 « 返回主页 脚本详解:generate-archive.ps1 »


    资源许可 · © 2004-2026 DragonRSTER · 最后更新于 2026年05月05日 22:35:36