DragonRster`s Void
Banner Image
目录
留言板架构记录

纯文本存储 + CGI 处理


整体流程

留言提交后的完整路径如下:





  [访客填写表单]  →  [CGI 脚本接收]  →  [写入 guestbook.txt]



         ↓



  [触发 rebuild-all]  →  [build.ps1 读取数据]  →  [注入侧边栏 HTML]



         ↓



  [部署新页面]  →  [访客看到留言]



            

留言后不会实时出现,而是在下一次构建时被编译进静态页面里(感觉有必要做个实时更新)。

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/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 "", $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-*dist//assets/*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 进程,但对于低流量站点来说完全够用,而且不需要任何框架依赖。


    « 2026年4月 · 最近在干些什么 返回主页 脚本详解:generate-archive.ps1 —— 归档、标签与搜索索引 »
    English

    搜索

    最新文章

    » 脚本详解:build.ps1
    » 脚本详解:gener...
    » 脚本详解:gener...
    » 脚本详解:rebui...
    » 脚本详解:gener...

    » 文章归档


    本文标签

    网站建设 CGI Python 教程


    功能

    » RSS 订阅
    » GitHub 源码
    » 返回顶部
    » 文章归档


    DRAGONRSTER
    CC BY-NC-SA
    © 2004-2026 DragonRster • Made with HTML • 本站支持IE5.5+
    最佳浏览分辨率:1024x768 • 最后更新于 2026年04月29日 02:31:07