分类: Site Articles

我和 AI 一起迁移一个十九年前的论坛:半年时间,从"生成代码"到"验证行为"

半年时间,比想象中的短,但又比想象中的长。

毕竟在大模型真正到“可用”阶段之前,谁也不会想去重构一个2008年的 PHP 中型项目。

本文包含 Github copilot 和 codex 的创作。

0.这是什么?

“口袋社区 PokeTB”是我在初中时候(~2007)与几个互联网上的网友开办的一个宝可梦主题的论坛。2008年的中国互联网,只需要一张 ICP 证书即可开办论坛,当时甚至还有类似于“我的领地(5d6d.com)”这样的“论坛站群”,注册一个账号你就可以获得自己的分站点。

image.png

以现在的数据说话思路,它至少还有14000+的主题(其中有很多 Pokemon Rom 修改的资料和工具)、以及超过28万的回帖。

image.png

这些朋友现在有的已经投身游戏行业,甚至有的曾经在神游(iQue)工作,现在已经移职到香港任天堂。

还有一些朋友,在聊天室退场,论坛贴吧刚刚兴盛的时候,自学 php,给论坛做插件。

image.png

更有一些朋友,当初为论坛设计顶头 Logo 和界面风格,现在有不少朋友已经是知名公司的插画师、独立漫画作者了。

image.png
image.png
image.png

1. 为什么要迁移旧系统

1.1 旧系统维护起来越来越贵

2007年的互联网上机器流量没有这么大。而时至今日,旧系统真的没有办法扛住现在的互联网流量。

那个时候,互联网 B/S 架构软件对于 MVC 的实践远没有现在成熟。当然,也有一些问题是 Discuz 6 特有的,后面21世纪后10年的 Discuz X 系列也以经典 MVC 架构重构过。我们没有 Follow 版本升级的原因有很多,主要还是时间精力,以及插件迁移的成本问题。

从 Discuz! 6的业务现状来看,问题很多:

1、模板、查询和业务逻辑混在一起。 Discuz 的模版引擎include/template.func.php 里的 parse_template() 读取 .htm 文件,把变量和 {template}、{subtemplate} 语法转成 PHP,生成 forumdata/templates/*_*.tpl.php。这套机制有编译缓存,代价是模板最终会变成可执行 PHP。业务入口同时还直接查数据库、准备展示数据:faq.php 在一个入口里根据 $action 查 FAQ、拼搜索条件、选模板;medal.php 一个文件管权限、分页、查询和申请写入;campaign.php 还要读配置、反序列化设置、拼外部 iframe URL。

这些文件未必不能工作。问题是数据准备、业务规则、页面输出之间没有稳定边界,改一个页面就得同时理解入口脚本、全局变量、模板语法、缓存刷新和隐含权限。

2、服务分层模糊。 pm.php 处理未读私信检查、登录判断、收件人查询、交易消息拼接,还直接更新会员表。modcp/editpost.inc.php 在一个 action 分支里完成版主权限判断、帖子查询、表单展示、主题和帖子更新。logging.php 同时管登录页面、验证码和安全问题、UCenter 登录、会员查询、会话状态和跳转。这样的代码能按历史路径跑,但没法靠类型、依赖关系和边界检查去理解。

3、数据库访问靠全局对象和字符串拼接。 Legacy 用 $db、$tablepre、$discuz_uid 这些全局变量做查询,很多 SQL 直接在入口里拼。faq.php 用 $keyword 拼 LIKE '%$keyword%',campaign.php、medal.php、invite.php、pm.php 也都在入口里拼查询和写入。旧代码通常会先过一遍 addslashes()、dhtmlspecialchars() 或 intval(),但这和参数化查询、明确的数据对象、可测试的 Repository 完全是两回事。

仓库里还留着几个 mysql_compat 兼容层,用全局 $mysql_link 把废弃的 mysql_\* API 映射到 mysqli。看得出当时的目标就是让旧应用继续跑,没打算动数据库访问模型。

4、安全和运行时机制背着历史包袱。 表单保护靠 FORMHASH、formhash()、Cookie 和入口级判断;common.inc.php 直接 error_reporting(0),顺手关掉 display_errors。这些机制不等于"完全不安全",但很多行为藏在全局初始化和约定里,排查问题时你说不清一个请求到底过了哪些保护。

5、源码里还有旧式动态执行和反序列化:UCenter 客户端用 eval() 动态实例化控制器;campaign.php 从设置里 unserialize() 配置;归档入口用 eval() 执行权限公式;模板里也有 {eval ...},viewpro_inajax.htm 就会执行显示函数和自定义作者信息。这些不能一刀切全删,有些可能是历史协议或数据格式。正确做法是逐项确认输入来源、实际用法和替代边界。

6、限流和资源保护跟业务入口耦合在一起。 发帖和私信限制由 checkflood()、$floodctrl、$maxpostsperhour、$lastpost 这些全局状态共同决定,发帖限流还会查最近一小时的帖子数。WAP 私信和回复入口各自实现了一遍表单 hash、时间间隔和写入逻辑。机制本身能表达历史业务,但你没法统一观察哪个入口受限、限流会不会误伤、一次写入失败后状态有没有恢复。这些边界是 Modern 慢慢拆出来的:入口、权限、限流、事务、任务进程,各自可验证。

7、缓存责任不清,部署时经常"代码更新了,页面没更新"。 模板编译产物、数据库缓存文件、Cookie 状态、UCenter 数据、页面输出,各归各的机制管。迁移中间我还遇到过持久卷里的旧 Twig 编译文件继续输出旧页面。后来我把构建层缓存、Twig 编译缓存、OPcache/FPM 状态、Redis 业务投影分开定义,轮换和验证也写进了发布流程。

8、GBK 编码与现代基建格格不入:2008年的 vps 配置还是 1CPU+256MB 内存+10GB 硬盘,而且当时的个人电脑配置也普遍受限,加上 IE6 横行,对于 UTF8 的支持并不是很好,所以当时大量的网站采用 gbk 编码以节约 B、S 两方资源,但随着基建(php、mysql)的退役,GBK 的支持也逐渐变成了问题。

Legacy 不是一无是处。恰恰相反,旧系统里存着大量用户用了多年、但从来没被正式写成规格的业务规则。迁移要做的不是推倒重来,是把还重要的规则从隐式代码和运行时副作用里解放出来。

1.2 这个论坛不该封存在某个人硬盘里

另一个原因更简单:论坛里装的不是我一个人的东西。

十五年,很多人发过帖、回过帖、传过图。作者、时间、权限、附件、上下文,这些数据凑在一起是一段共同记忆。

如果它只能靠一台旧电脑、一个没人敢动的 PHP 版本、一个唯一掌握全部上下文的人活着,那它其实已经半封存了。数据还在,普通用户访问不稳,维护交接不了,将来也没法安全地继续跑。

所以这次迁移不是"把旧代码改漂亮",更不是证明我能重写一个论坛。它更像数字遗产维护:把已经发生过的内容和行为尽量保住,同时把系统挪到一个别人能理解、能验证、能部署、能接着维护的地基上。这也是为什么项目坚持保留历史数据、已确认的业务语义和用户界面,不借迁移之名清理旧记录、改交互、重设计数据库。

2. “修旧如旧”:表面是升级 PHP,实际是保住一段业务历史

项目对象是一个跑了十九年的中文论坛,基础系统是 Discuz! 6.1F。目标不是重新设计一个论坛,而是在尽量保留历史数据、业务语义、权限关系和用户界面的前提下,把运行环境和代码结构带到 PHP 8.3。

重构后的系统是 PHP 8.3 的模块化单体:Composer、PSR-4/PSR-12、PDO、Router、Middleware、Twig。数据在 MySQL 8.4,Redis 管会话和部分缓存,附件走对象存储,应用和任务跑在 Docker 里。

听着像一次标准的老系统现代化。难的地方不在旧语法改新语法,在于旧系统里有大量没写进正式规格的规则:

  • 有些字段的含义已经被历史数据固定了;
  • 有些权限不只看当前角色,还看作者、版块、主题状态和已有操作;
  • 积分、余额、库存、日志、通知必须在正确的事务边界里一起成功或一起回滚;
  • 一个旧入口可能包含多个 action(modcp.php),不能因为入口名字相同就统一跳到同一个现代页面。

至于为什么不用更新、更酷的现代技术栈,比如 node 甚至 go,继续向下看。

3. 最初的教训:架构迁移的边界,和生成很多文件不等于完成迁移

今年一月的时候,我动了重构这个论坛的念头。一方面,yadex 和 yahoo 的假冒爬虫让机器频繁宕机,另一方面,discuz 的防 cc 是以 mysql 作为数据库的(会把 sid 落库),海量的异常请求把 mysql 都打垮了。加上去年(2025)一年, cursor 带给我的体验太神奇了:

那肯定是先用自己现在最熟的东西:来,让 claude code 重构成 nodejs 项目。很直接:按模块和周计划,让模型生成 Controller、Service、Repository、测试和完成报告。推进很快,新系统骨架很快就有了,然后灾难来了。

2月的模型是什么: Claude sonnet 4.6、GLM4.7(对,5都还没上),Openclaw 也还要半个月后。大家对于 Agent 工程也懵懵懂懂,自然就是一坨灾难。

Claude code 在把 plan 和 GLM Coding Plan 都烧完之后,告诉我搞定了。

结果可想而知。

这个结果给我的最重要的输入是,对模型能力的判断。也在那时,我确定了当前节点的目标:先别换语言了,让它的架构跟现代开发框架一致,“修旧如旧”。下一步再说怎么改。

很快我确立了三个黄金原则,直到现在也留在了这个阶段的项目里,帮了大忙。

1、零改表原则:一个 BS 架构的系统包含前端、后端、缓存和数据库,缓存随便扔,如果要动前后端,意味着数据结构可能要有变化,那如果数据库也一起修改了,就意味着无法做校验。虽然确保不动数据库可能会导致模型层产生垃圾兼容代码,但这是阵痛。

2、最终行为一致:虽然模版引擎从 discuz 的 template 迁移到了 Twig 这个现代引擎,但页面视觉、行为、交互应当严格保持一致。

3、TDD 原则:虽然这半年很多人已经进化到了 SDD 模式(即 Spec 驱动开发),但事实上,旧系统翻新这件事,有明确标的,采用 旧系统-写用例-改新系统-完善用例 这样 交替迭代系统侧和测试侧的方法更为有效。截止项目上线,目前维护了14000+ Unit Case、3000+行为 Case 和 300+前端 case。

虽然这些原则在大概3月就确定了,但是当时的模型确实能力不足,导致产生了一个让我很头疼的问题:文件按照规划的模块都生产了,然后 claude 报告已经完成,然而:

一是文件存在经常被当成能力存在。类、路由、单元测试都能写出来,但用户真走一遍"进页面、提交表单、发生写入、跳转、再读取"的链路,接线错误就露出来了。

二是现代设计习惯会悄悄改写旧业务。模型看到一个不好看的旧字段,就顺手推一个更"合理"的新字段;看到一个怪权限分支,就当成 bug;看到旧的 CSRF、编码或状态处理,可能直接换成一个看起来更现代的实现。

三是测试有时验证的是实现布局,不是用户行为。比如某个共享组件已经正确提供了路由,测试却要求某个特定字符串必须出现在页面源文件里。测试是失败了,但失败的对象是代码组织方式,跟业务结果没关系。

所以,我还是引入了行为(业务能力)迁移判断流程:从"按文件补齐"转成"按业务能力迁移"。能力的单位不再是某个 PHP 文件,而是一个用户或后台操作者能完成的动作,包括入口、角色、状态、读写、外部副作用和生命周期。

这个区别很关键。比如说,旧的 poll.php 不是一个能力。创建投票、投票、查看投票人、编辑、关闭、删除、个人列表,才是不同的能力。它们可能共享代码,但权限、状态和验收标准不一定共享。

4. 三种权威和目标的迭代:虽然已经可以自动化了,但前提是人依然很重要

迁移过程中,另一个浪费了很多时间的是,搞清楚"事实""目标"和"授权"。现在分三类:

实际是什么。 以可达源码、配置、数据结构和运行证据为准。历史文档可以帮你导航,但文档里的一句话不能压过代码和实际行为。

目标是什么。 以当前已确认的产品语义和领域契约为准。Legacy 是重要证据,但不是无条件的真理。旧系统的漏洞、已批准退役的入口、损坏数据的保全策略,分清楚什么是“原有设计”、“缺陷”,并能判断“是否保留”,仍需要大量的人工参与。

这次能做什么。 以当前任务的授权边界为准。普通实现和可逆修复可以继续;新的数据删除、结构变更、不可逆副作用、生产操作,要单独确认。

所以在6月、7月两个月,我基于这个方法明确了很多围栏,并且与模型专门进行了一轮历史功能确认,以明确功能边界。可以说,如果没有人工输入的判断,就没有下面完全脱手的实现。

在8月份,codex 的 /goal 稳定了,我也让 5.6-sol 在走查修复过程中使用了一个 15day+ 的 goal。不得不说,模型的进步速度实在是太快了,goal 让我真正享受一把“脱手”的感觉:

image.png

最后的这两个月提交量暴增(我从来没有在 github 上每天100+PR),而且模型已经可以稳定的自行闭环走查任务了。

5. 最难的不是分层,是守住旧行为的边界

/goal 解决了“脱手”的问题,但也让过程监控和结果是否漂移变得更难观测。

5.1 数据库"少改"不等于业务简单

项目有零改表原则,但它不是"永远不许改表"。真实含义是:新增结构之前,先核对 Legacy 的同功能表、字段和文件存储;确实要变,必须有明确的结构决定和兼容方案。

这么做的理由不是迷信旧表。历史数据本身就是系统的一部分,一个看着干净的新字段可能解释不了旧记录,一次批量清洗可能毁掉用户看不见但有意义的状态。

5.2 跨域写入必须有明确边界

论坛、积分、银行、附件、通知、ZPet 之间联动很多。ZPet 最后用的是进程内插件加唯一的同步事件总线,没有让核心业务直接调插件内部逻辑。

事件通道也不能混用:有的必须跟根事务一起提交或回滚;有的要求同步且只能有一个处理者;有的只做只读收集;有的用于跨事务投影。这个区分比"用了事件总线"重要得多。

5.3 模板迁移不是替换括号

Legacy 模板的路径是 .htm 经旧模板引擎生成可执行的 .tpl.php,运行时加载。迁到 Twig 后:

  • 旧变量、条件、循环、子模板改成 Twig 的输出、控制结构和继承;
  • 模板里混着的业务计算挪到控制器和服务;
  • 全局变量变成明确的数据输入;
  • 翻译、URL、资源路径、CSRF、BBCode、插件插槽全部重新接线;
  • Legacy 的布局、类名、语言和样式语义尽量保留;
  • 在线模板编辑和旧的可写执行产物退役,模板变更交给源码和发布流程。

6. 真实浏览器比"AI 说完成了"可靠

项目里最有效的验收方式之一:把用户动作真走一遍,同时看页面、数据库、对象存储和日志。

很多问题只有真实链路里才会出现:

  • 控制器写好了,服务工厂接错类型;
  • /bbs 挂载路径漏了,源站检查正常,公网路径不对;
  • 下载内容正确,原始文件名丢了;
  • 表单按钮值跟 Legacy 不一样,服务端直接拒绝;
  • 旧页面 CSRF token 过期后,失败回显把用户输入丢了;
  • CSS 和头像这些公共资源也走完整应用初始化,页面本身没问题,但整个请求瀑布很慢;
  • 测试库里的外围事务掩盖了真实请求中事件准备时序的错误。

不同证据回答不同问题:单元测试验证局部规则,集成测试验证组合,静态检查验证边界,浏览器验证真实装配和用户路径,发布后检查验证实际运行的版本。

7. 架构和发布:重点在边界连续

Modern 的架构可以概括成模块化单体:

  • 前台、AdminCP、ModCP 用 Twig 组织页面;
  • Front Controller、Router、Middleware 处理入口、身份、权限、CSRF 和输入边界;
  • 业务服务管论坛、用户、私信、积分、银行、附件和管理能力;
  • Repository/PDO、Redis、对象存储、邮件、任务是基础适配;
  • ZPet 通过类型化契约和唯一的同步事件总线协作;
  • Legacy 行为证据和可观察回归是系统的两根柱子。

生产部署不是把容器启动起来就完了。PHP 镜像和 Nginx 镜像分开构建,依赖层和业务源码层分开,基础镜像按摘要固定,OCI 层按 SHA 校验并复用。生产里 PHP-FPM、Nginx、常规定时任务、投影/邮件任务、附件隔离任务分开跑;数据库、Redis、备份各有独立的数据生命周期。

发布的流程:完整质量验收和静态验收先绑定最终 main 提交,本机 pre-push 登记证据;GitHub Actions 按 AMD64 构建,做依赖审计、SBOM、漏洞扫描和隔离 smoke;服务器核对 OIDC、源码 SHA、制品摘要和运行身份,然后才切换五个应用容器。失败就回退到上一份合规的 main 制品,重新轮换缓存、重载 FPM、验证入口。

这里得诚实:这是原地 Compose 更新,不是双集群、不是金丝雀、不是全业务零停机发布。两个入口的 HTTP 检查只能证明入口健康,代替不了完整业务验收。镜像回退撤不掉新版本已经写进数据库的数据,更不等于数据库回滚。

8. 性能教训:高配开发机会让错误的测量显得可靠

本机是 M5 Max、128GB 内存、2TB 硬盘。开发体验很好,盲区也明显:很多问题在开发机上不显形,部署到资源更有限、还跟 WordPress 和百科共享 CPU 的生产机后才暴露。

更根本的问题不是机器快,是早期测量的对象不完整。有过性能报告只测单条数据库读取,还把顺序循环叫成并发。这些数字能证明某个局部片段快,证明不了首页响应、FPM 队列、浏览器首屏、真实并发没问题。

上线后的分析显示,成本主要来自请求链里的重复工作:路由身份构造、插件和事件元数据检查、头像清单处理,还有 HTML、CSS、头像等资源各自启动一遍应用链路。一次优化把重复的路由身份构造从约 43,295 次降到 1,790 次;另一次把交替原型路由安装的 CPU 中位数从约 193ms 降到约 44ms;CSS 路径的 CPU 从约 333ms 降到约 93ms。

现在的经验是:从第一个纵向切片开始,就在接近生产的资源约束下测完整 HTTP 链路,分别记录启动、路由、查询、渲染、附属资源、FPM 排队、浏览器首屏。性能不能等全部功能迁完再安排。

9. 回头看:Claude Code、GLM、Copilot 和 Codex 分别改变了什么

这个项目先后用过 Claude Code 配 GLM 模型、Github Copilot 和 Codex。

工具换来换去,最大的启示是一条:工具可以换,共享事实不能依赖工具的记忆。

先是 Claude Code。早期模型也不行,工具也不完善,所以单体构建、单体测试,人工回归。但它至少给人了一个反馈:这事搞不好真的一个人能干。它更像快的代码生成器,帮忙搭骨架、补局部实现。项目复杂度上来之后,真正重要的工作变成了:从 Legacy 提炼行为证据;对照 Modern 的真实装配;找出"看起来合理"但不符合历史语义的实现;把一次线上或浏览器发现固化成回归;检查源码、测试、镜像和生产运行身份是否一致。

Github Copilot 很像 multica :你给他一个 issue(pr、issue、comment),它自己有 workspace,可以构建检测,推送上线。但是它有跟 Codex /goal 类似的问题:过程不可监控,猛猛几个小时出来一个大PR,你根本不知道它在干什么。

Codex 是能成功上线的最终推手,不只是模型能力,也基于这半年超过所有人类认知的模型、Agent 工程的发展——它真的能用在真实项目里了。

10. 下一次迁移,我会提前做什么

再来一次的话,我不会照搬这个项目的全部文档,也不会默认照搬 PHP 分层、零改表、四类事件或现有 Docker 拓扑。方法复用,业务重新调查。

第一天先立五样东西:

  1. 项目适配配置:代码位置、运行命令、数据隔离、不能碰的资源、生产边界;
  2. 能力清单:按入口、角色、状态、动作列出真实迁移表面;
  3. 最小行为证据:源码、配置、样例输入输出、已批准差异;
  4. 纵向切片:从入口到存储、响应、再次读取,走通一条真实链路;
  5. 真实性能和缓存契约:第一个切片就定好测量范围、资源预算和失效方式。

然后让程序帮着判断哪些结论需要重看:Legacy 摘要变了,重看关联事实;Modern owner 变了,选受影响的回归;产品决定被替代了,更新预期;环境不匹配、测试没跑、源码漂移,撤销"当前通过"状态。

理想的最小技能包只要四个动作:discover 发现入口、能力、来源和未知项;characterize 把行为、边界和差异固定下来;migrate 实现一个有界且已授权的纵向切片;verify 以源码身份为起点,给出证据等级和真实缺口。

模型应该拿到的是任务包和证据索引,不是几百页互相覆盖的历史记忆。

结语

遗留系统迁移最危险的时刻往往不是代码报错,是代码看起来已经完成。单元测试过了,但没有真实入口;新表有了,但解释不了旧数据;页面能打开,但丢了一个用户依赖多年的边界行为;镜像能部署,但不是刚才验收过的那个版本。

这次项目让我接受了一个不怎么激动人心但更有用的结论:AI 的价值不是让迁移变成自动驾驶,而是让人有更多时间花在真正要紧的判断上——哪些行为必须保留,哪些应该改变,哪些差异已经被批准,什么证据足以说明这次改变没有越界。

这些判断写成能力、契约、回归和发布证据之后,Claude Code、Copilot、Codex,包括下一代完全不同的工具,都只是执行和检查这套方法的不同入口。

服务器再搬家

昨天刚好是7月13号,距离我把主服务器从 sakura vps 迁出刚好整一年。这一年里我使用的是论坛成员推荐的海星云服务器。虽然存在比较大的丢包情况,但在七牛 cdn 的加成下总体也还算比较平稳。但在上个月,因为备案失效而被迫从中国大陆地区迁出的口袋维基迁移到这台服务器的时候,每个月 1TB 的流量配额就显得特别捉襟见肘了。

VPS 6月流量总图

为此,我还特意把剩余的几个月加钱升级了高配,就为了多买一点流量。临近一年整,靠升配续命终归不是正道,于是服务器再次搬家就成为了一个议题。

这不,好容易有个闲暇时间,赶紧把家搬了。

这次选用的是 Wenjing network 旗下的 HostKVM 。看 IP 分配,感觉跟之前海星在同一个机房,但丢包情况得到了不少改善。按照老规矩,观察几天,看看情况。

二〇一九年三月

发现已经三个月没有更新博客了。过去的这三个月发生了好多事情,让我意识到能够一直保持愤怒和好奇心是非常困难的事情。不知道是年龄大了还是事情真的太多,“精力有限”的感觉真的是一直在围绕着我。这种状态让我感到很不舒服,我需要尽快的调整一下自己了。

从15年误打误撞开始做 maimai 的魔改以来已经过去了很久,久到我甚至都很久没有放精力在这上面了。但是还是稍微做了些事情在上面。最近可以拿的出来的是我开始出售主机的替代硬盘。这也算是唯一的相对比较合法的东西。

SSD for Ringseries.点击这里可以去购买

而对于我个人来说,一月年前很忙,二月过年回来就搬家,三月份一抬头也已经过去了一大半儿了。人是越来越忙,但是碌碌无为的感觉却又越来越明晰。因为合租的家伙买了辆车,我家也基本达到了人均一辆车的水平。而我最近已经开始选择摩托车通勤了。如果算上摩托车的话人均大于一辆车,也可以说是北京高端通勤人群了。

我的通勤车,铃木 GSX250r

三月是我司的晋升季,虽然天天在完成日常工作之外还要去准备各种材料,但也不是完全埋在工作里面的。比如这个:https://post.smzdm.com/p/akmrxplk/ ,是我国一些伟大骗子制造的电子垃圾。虽说是电子垃圾,但是只要足够便宜,那就可以买回来玩一下。是的,我也跟风买了一台。昨晚拆了一下,把不能用的部分拆下来装了个台式机,准备找个机会处理掉,机箱打算上一套 itx 来跑 nas 。垃圾还没收全,那就先放几张图作为预告吧。

我收的这台成色相当不错,主板甚至一点灰尘都没有。已经跟其他的淘汰垃圾装好了。

嗯,希望讲这篇的文章不会拖太久~

替换 Discuz! 6 的默认验证码

PTB 今年已经是第 11 年了,代码也是 10 年前的代码了。
这次又遇到什么问题了呢?就是我们的验证码机制被攻破了。
而且其实挺遗憾的,因为 PTB 最初的安全设计是严进宽出,即你注册需要有相当繁琐的步骤和漫长的验证期,之后几乎没有什么限制。

然后很尴尬,我们被撞库了。

就是前一分钟我们还在微信群里面谈笑风生一分钟之后刷出来发现对面那个人的 ID 在论坛灌了 1000 多个台湾找小姐这样。

然后我就按照之前双子的思路打开了验证码。

但是很尴尬, Discuz 6 有一个很严重的逻辑漏洞。这其实也不是逻辑漏洞,是当年大家对 ajax 的理解都不到位而已。

按照惯例,我们来读代码。

DISCUZ_ROOT./ajax.php:64


...
} elseif($action == 'checkseccode') {

	if($seclevel) {
		$tmp = $seccode;
	} else {
		$key = $seccodedata['type'] != 3 ? '' : $_DCACHE['settings']['authkey'].date('Ymd');
		list($tmp, $expiration, $seccodeuid) = explode("\t", authcode($_DCOOKIE['secc'], 'DECODE', $key));
		if($seccodeuid != $discuz_uid || $timestamp - $expiration > 600) {
			showmessage('submit_seccode_invalid');
		}
	}
	seccodeconvert($tmp);
	strtoupper($seccodeverify) != $tmp && showmessage('submit_seccode_invalid');
...

以及

DISCUZ_ROOT./include/global.func.php:888


function submitcheck($var, $allowget = 0, $seccodecheck = 0, $secqaacheck = 0) {
	if(empty($GLOBALS[$var])) {
		return FALSE;
	} else {
		global $_SERVER, $seclevel, $seccode, $seccodedata, $seccodeverify, $secanswer, $_DCACHE, $_DCOOKIE, $timestamp, $discuz_uid;
		if($allowget || ($_SERVER['REQUEST_METHOD'] == 'POST' && $GLOBALS['formhash'] == formhash() && (empty($_SERVER['HTTP_REFERER']) ||
			preg_replace("/https?:\/\/([^\:\/]+).*/i", "\\1", $_SERVER['HTTP_REFERER']) == preg_replace("/([^\:]+).*/", "\\1", $_SERVER['HTTP_HOST'])))) {
        		if($seccodecheck) {
        			if(!$seclevel) {
        				$key = $seccodedata['type'] != 3 ? '' : $_DCACHE['settings']['authkey'].date('Ymd');
        				list($seccode, $expiration, $seccodeuid) = explode("\t", authcode($_DCOOKIE['secc'], 'DECODE', $key));
        				if($seccodeuid != $discuz_uid || $timestamp - $expiration > 600) {
        					showmessage('submit_seccode_invalid');
        				}
        				dsetcookie('secc', '', -86400 * 365);
        			} else {
        				$tmp = substr($seccode, 0, 1);
        			}
        			seccodeconvert($seccode);
        			if(strtoupper($seccodeverify) != $seccode) {
        				showmessage('submit_seccode_invalid');
        			}
				$seclevel && $seccode = random(6, 1) + $tmp * 1000000;
        		}
			if($secqaacheck) {
        			if(!$seclevel) {
        				list($seccode, $expiration, $seccodeuid) = explode("\t", authcode($_DCOOKIE['secq'], 'DECODE'));
        				if($seccodeuid != $discuz_uid || $timestamp - $expiration > 600) {
        					showmessage('submit_secqaa_invalid');
        				}
        				dsetcookie('secq', '', -86400 * 365);
        			}
        			require_once DISCUZ_ROOT.'./forumdata/cache/cache_secqaa.php';
        			if(md5($secanswer) != $_DCACHE['secqaa'][substr($seccode, 0, 1)]['answer']) {
        			        showmessage('submit_secqaa_invalid');
        			}
				$seclevel && $seccode = random(1, 1) * 1000000 + substr($seccode, -6);
        		}
			return TRUE;
		} else {
			showmessage('submit_invalid');
		}
	}
}

不难看出,其实对于验证码的过期,后台的判断只是基于时间(600秒)和是否是属于这个用户。
这其实是有很大的问题的,因为没有重试次数限制,对于不复杂的非中文验证码,一秒钟发出几千个请求cover掉所有数字和大部分字母组合是很轻松的事情。
因此干脆就把它从代码里面摘掉。

主要修改的是下面几个文件。因为现在就是这么做的,因此具体的细节就不展开说了。

include/javascript/post_editor.js
摘掉里面对原有验证码的前端校验

template/seccheck.htm
摘掉原有验证码的展示代码,同位置增加极验的前端代码

ajax.php
屏蔽掉前端校验接口
global.func.php
common.inc.php
摘掉原有验证码后端校验的内容部分,保留 Cookies 校验部分
增加极验后端校验代码

总结一下,其实 discuz 6 在代码设计上真的是完美无缺的。这么老的代码改起来也没有什么不适感(最起码比在公司里吃的那些 s**t 要好得多太多了)。基本就是搜“submit_seccode_invalid”就可以解决这次的问题。
总之双站都是靠极验老版本去 cover 机器人了,毕竟防御价值没那么高,就不去针对 geetest v3 做适配升级了。
屏蔽掉机器人就已经解决绝大部分问题了。

一次古典互联网服务器迁移记录

严格上说,我们这一代前站长(因为现在事实上没有高活跃度会员),赶上的是 Web2.0 刚刚开始起步的那段时间。
谁能想到两三年前 PHP 已经被划作前端范畴了,而最近这一两年前端技术又这么发达了呢。
然而既然决定了保留这些互联网遗迹,那就得让他们在目前为止最好的工作状态下工作。

2007 年春,口袋根据地站长 nfopo 宣布暂时关站,同期,我们筹划成立口袋社区 Poke The BBS (当时英文名叫 Pokemon The BBS)。那时候开一个网站的成本是非常低的。互联网刚刚跨进 Web2.0 ,人人分享的平台还是每人在计算机上创建自己的网站。 CNNIC 联合国内的域名服务商推出了 1 元甚至 0 元购买 .cn 域名的活动。口袋社区最早是以 poketb.cn 作为主域名的,后来购入了同名的 .com 域名,并沿用至今。
poketb.cn whois
最早的口袋社区是 Discuz! 4.1F ,服务器是现在仍然在僵尸的梦游科技美国合租。彼时还没有智能 DNS 和 CDN 这样的高科技玩意儿,以至于现在 PTB 的代码里面还有这样的历史遗迹。
ptb 分站列表
其实后来 PTB 的命途也比较坎坷。无非就是我们比口袋根据地的站长年纪小了几分,最后大家都没有逃过高考这一关。从梦游科技迁出后, PTB 先后托管在 iFastNet 、 vultr vps 上。因为国内互联网环境日趋复杂,导致总有一些会员无法成功访问网站。这也就是导致论坛会员日趋减少的一个重要原因。
我是大概 2013 年的时候决定重新将网站开放出来的。当时的想法也比较简单,就是无论自己当年多么中二,这段记忆总还是要在的。毕竟还是一个可以向后人吹牛逼的资本:“看,你爹当年就是对这个玩意儿上瘾了” —— by liuyanghejerry。
之后在潘达的帮助下我将服务器迁移到了日本的 sakura vps 。樱花当年的速度是真好,服务也比较稳定。
sakura vps
只不过美中不足的是,樱花也是个古典 vps 提供商。它们的账户甚至还需要人工去确认日本本地地址。而 vps 除了稳定之外,在性价比上更是一塌糊涂。前一年我使用单台机器承载了所有流量。这在一台双核 1G 内存的机器上来说真是个大型的挑战。要知道 MySQL + PHP-fastcgi 可是吃内存磁盘的大户。机器常常被拖得十分卡顿。于是在大概一年以前我拜托潘先生另外购买了一台同机房的 sakura 节点用于单独跑 MySQL ,并拿来做 SS 节点。
现在来看这个决策是很正确的,虽然多花了一点钱,但是可以有效承载更大的用户流量。这也为成功托管口袋双子星打下了一个良好的基础。
然后一年前接手了口袋双子星,并为其在口袋社区服务器的 php5.6 环境下运行做了大量的改动。
近几年,互联网,特别是移动互联网进一步普及,使得中国的互联网环境空前复杂。面试、考试中简单的从一台计算机 A 连接到服务器 B 这样理想的情况已经不复存在(虽然看上去还是那样的)。于是在去年晚些时候我又花了一些时间为双子、口袋社区部署了一个用于 CDN 的域名 suicune.cn 并进行了加速。
然而,还是很慢。发现原因是 sakura vps 到大陆的速度变的越来越差,于是就有了这次迁移。
这次迁移出于平衡的考虑(因为数据库节点不涉及到访问中国大陆,因此只要在日本就好),我选择了一台仍然在日本但是速度相对较好的 vps 。经过测试后有中文客服,并且速度还不错的海星云成为了我的首选。
接下来的事情就是测试一下稳定性,以决定是否继续使用了。

============2018.06.04更新。

先说结论吧:总体来说服务的稳定性没有 sakura 好。
毕竟是二道贩子嘛,比不过自建线路的地头蛇的。刚好在敏感时期,刚迁移完就发现有奇怪的问题。这次迁移之后我对 vps 本身的改动是有三个:其一,升级了 linux 内核到 4.3 ,默认开了 bbr ;其二,调整了 php5.6-fpm / php7-fpm 的编译参数,这个测试没有什么影响; 其三,测试几个小流量站点直接 Caddy - PHP ,去掉了中间的 nginx。第三条这个测试后来摘掉了,因为发现维护起来实在是太麻烦了。
总体来说还是维持 外部流量 - Caddy - Nginx - php5.6/7 - 代码 的这个逻辑。按理来说迁移应该是无感的, 因为环境配置和代码位置甚至临时文件位置都是一样的。但是依然出现了奇怪的丢包。
现在在怀疑是特殊时期导致的,等这两天过去之后再确认一下吧。

nginx前端代理导致nginx暴露监听端口问题解决

其实我还是挺想吐槽一下我国的网络管理制度的。一刀切的政策导致很多爱好者交流的地方直接就毁灭于无形之中了。比如说中华相声网,再比如更多名声更小的论坛。

中华相声网

其实这个事情很简单,在我国开论坛需要企业资质+24小时值守,这两条我我们很多爱好者性质的论坛就完全没戏。
当然有些论坛我不知道是怎么备过案的,比如某新生代,再比如某吧。

算了,不扯非技术,来聊聊应付这一规定中间的一些技术难题吧。

就在前两天,我们伟大的电信网络开启了前所未有的海外网站白名单制度。不仅是在黑名单上的网站、 IP 无法访问,其他的未进行白名单备案的服务器和 IP 也只能有限的访问 22 80 443 这几个端口(经过实测有些灰色 IP 比如本机甚至只能访问 443 端口)。这就逼着我把拖延症拖了快一年的全站升级 https 的事情放上议程。

经过花花的推荐,我选择了 Caddy 对 Nginx 外面包裹一层的方法来解决这个问题。

小课堂:
Caddy是个用 Go 语言开发的轻量级 http 服务器,特点是内置了全自动续命 续期的 Let's Encrypt 服务。而后者是有效期较短的免费 https 证书服务,旨在进一步提高互联网的安全等级,最重要的还是免费。

于是按照教程写了一万个 redirect 和 https 域名适配,之后发现一个很严重的 bug 。

根据这位仁兄博客的说法,具体的问题是:

但是访问子目录时,除非在子目录后面再加一条“/”,否则就会遇到网址自动重定向至Nginx监听的端口。假设你Nginx站点监听的端口是123,你本来访问的地址是http://domain.com/wp-admin,会自动重定向至http://domain.com:123/wp-admin

这位仁兄当然也给出了解决方案,也就是在 nginx 的 http 段增加配置:

port_in_redirect off;

然而在本机未生效。后来发现是 nginx 版本过低。具体的发现过程如下:

阅读 nginx 官方文档对这个参数的定义

http://nginx.org/en/docs/http/ngx_http_core_module.html#port_in_redirect

然后查看其关联指令absolute_redirect ,发现版本是1.90。
于是怒升1.8到1.12.2(上周刚发布的),搞定。

将博客的静态资源迁移到了七牛

最近实在是比较忙。高估了自己的能力。
现在主要是编码速度实在不尽如人意。以后针对这一点好好做一下训练吧。

最近抽空把博客速度慢的问题好好解决了一下。主要使用七牛云来作为主要的托管媒介。
细分一下这个需求的话分为如下两点:
1、域名未备案导致的解析速度缓慢,由于页面内存在比较多的本地资源,导致加载速度感人,手机上的表现是白屏,电脑上也时不时抽风。
2、图片资源使用七牛默认域名(测试域名),导致新版Chrome认为资源不安全,拒绝加载。
3、JS类库来自本地或海外cdn,速度感人。

于是花了大概十分钟的时间解决了一下这个问题。不得不说,七牛的工具虽然做的比较烂,但是能用。

现将步骤记录在这里。

1、WP侧安装插件七牛云储存
插件链接:https://wordpress.org/plugins/wpjam-qiniu/
也可以直接在后台搜索安装。
正常配置,填写AK和SK, 同时为你的储存区域分配一个SSL域名(这是要收钱的,不过4毛钱1G也就那样了吧),填写为你的加速域名

2、在服务器上下载七牛迁移工具(应该是同步工具)
https://qiniu.kf5.com/hc/kb/article/68954/
按照页面说明初始化,并将wp-contents和wp-include目录中的静态资源(css、js)按照路径,以分别的目录前缀(wp-contents/和wp-include/)上传到七牛云的储存空间。

然后就可以正常的享受这个加速了。

介绍下搭建这个博客遇到的坑

从决定开始重新搭个博客一直到这玩意上线,前前后后花了我大概两个多月的时间。虽然中间还有例如做 mai 硬盘和其他私活,但是我还是觉得时间有点太久了。整理的概要文档都快忘光了,趁彻底忘记自己写的是什么之前把一些坑记录下。

1、vps 的选择

首先就是服务器的选择了。因为个人使用,加上对网络要求比较高,因此我一开始就把目标放在了千元每年左右的vps上。首先一个大坑就是没有选择国内相对稳定的免备案vps,先后尝试了 linode、Diahosting、OneAsia等相对还稍微比较像样的vps,但是无一不在三天后获得gfw认证。尤其是linode,基本开半个小时ssh都被墙掉了。最后还是经过朋友推荐来 aws 开了一年免费套餐,反正一年后正常续费就是了。

这个 aws 偶尔也是会有墙掉的情况,具体症状是我昨天部署完 wordpress 之后,安装程序会导致与服务器的ssl连接莫名其妙断开,之后就连不上了。算了,目前还可以,凑合用。

对了,我选择的是韩国机房。

2、nginx的编译

vps开好了之后,就是环境的搭建了。刚开始踩的大坑是vps内存不够2G是编译不了 mysql 的。装了一堆东西编了一堆垃圾之后,我重新开了新的镜像从零开始。

首先是 nginx ,我参考的编译参数是来自于这个网页的说明:https://my.oschina.net/liucao/blog/470241 ,为了防止原网页挂掉,我把网页的原文粘在下面。

$ ./configure \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-http_auth_request_module \
--with-mail \
--with-mail_ssl_module \
--with-file-aio \
--with-ipv6 \
--with-http_spdy_module \
--with-cc-opt='-O2 -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic'

第一个坑就来自于编译参数,我现在使用的 nginx 已经是1.11.3,其中的-with-http_ssl_module已经变成了--with-http_v2_module 。 这也证明httpv2正式成为一个新的标准了。

3、php7

选择php7的原因是它真的有了非常大的提升。

之前编译 mysql 给我留下的阴影实在太大了,我转而选择为 yum 增加源,使用 yum 进行安装。

主要参考的文档是:http://blog.csdn.net/dxywx/article/details/50609137

这里有个文字的坑,就是这个博主的代码段里面有很多特殊字符被转成了全角,别的还好。

然后关于源本身和其他插件的安装,需要把 yum install php-**** ,全部都替换成 yum install php70w-**** 。 别的没有什么特别需要注意的了。

4、分区挂载

aws 的定制 centos 确实针对自身做了非常多的优化,但是有些优化也确实让人很摸不着头脑。比如外挂的储存卷,本意的设计是方便快速迁移,但是我一开始真的对着文档蒙逼了半天。特别大的坑也没有,就是默认分区格式是 ext4 ,fstab需要特别指定一下。

5、安全区

我也不知道是不是我遇到 bug 了,之前明明给这台 vpc 开放了80 和 443 端口,但是就是连不上。删掉安全区重新分配后就好了。

6、SSL证书

首先是 openssl 请务必升级一下。aws 带的这个还是1.0.1e,这是我前东家都放弃的版本,包含了完整的两个心跳漏洞。加个源1分钟就更新好了,别省这点事了。

然后说说证书,我选择的 SSL 证书来自ssls.com 。这家是以出售廉价 SSL 证书著名的。我看中的是它签发的根机构是 COMODO 。价格也还好,我分别为woodu.me 和 git.woodu.me(准备装个gitlab)购买了证书,一共花了大概200多块的样子,各三年。因此我觉得价格还可以接受。

购买的过程中基本没有什么要注意的,只有一点,域名认证的时候,推荐选择的是基于域名管理邮箱的邮箱认证,这个速度很快,基本确认了邮件证书就可以下来。我之前主站选择了放置文件进行 check,结果因为编码问题文件不符,还专门去联系了客服帮我手动通过。

(更多…)