v1.11.0 源码导航索引
按请求链路、安全边界和存储职责导航 Short_NURL v1.11.0 源码
本页对应 URL_PHP_v1.11.0。它不维护容易随重构失真的“函数总数、调用总数、锚点合计”,而以文件职责、入口、关键函数和数据流作为稳定导航。源码中的 @外接口_#、@关键_$ 等注释仍可用于全文搜索,但本文不承诺手工编号连续或数量恒定。
一、从公开路由进入
| 路由 | PHP 入口 | 认证 | 主要去向 |
|---|---|---|---|
GET /api/ping | api/ping.php | 无 | 直接返回 {ok:true},不加载业务 bootstrap |
POST /api/create | api/routes/create.php | 标准链路 | 验证、去重、双 Store 事务、热同步 |
POST /api/delete | api/routes/delete.php | 标准链路 | 冷删除后热删除 |
GET /api/list | api/routes/list.php | 标准链路 | 读取冷存储并过滤过期临时条目 |
GET /api/stat | api/routes/stat.php | 标准链路 | 冷计数权威,热计数诊断 |
POST /headless/api/create | headless/create.php | 无头链路 | 同类创建逻辑,增加服务 Key/len |
POST /headless/api/delete | headless/delete.php | 无头链路 | 冷删除后热删除,机器可读错误码 |
GET /headless/api/list | headless/list.php | 无头链路 | 冷存储列表 |
GET /headless/api/stat | headless/stat.php | 无头链路 | 冷计数权威,热计数诊断 |
GET /headless/api/get/{code} | headless/get.php | 无头链路 | 永久优先的单条冷存储查询 |
路由映射和长度约束从 backend/nginx/nginx.md 开始看。该文件还包含公开目录屏蔽、API/跳转限流、内部 18500 端口、PHP-FPM 映射、1-5 位跳转规则和 worker 初始化入口。
二、认证链路
标准链路
api/routes/*.php
-> api/common/bootstrap.php
-> auth_extract(RAW_INPUT, "api")
-> auth_verify(raw, "api")
-> KeyStore::verify()无头链路
headless/*.php
-> headless/bootstrap.php
-> auth_extract(RAW_INPUT, "headless")
-> auth_verify(raw, "headless")
-> KeyStore::verify()| 文件/函数 | 阅读重点 |
|---|---|
api/key/auth_extract.php / auth_extract | 标准链路只接受 X-Token 和 body.key,Header 优先;无头只接受 X-Headless-Token |
api/key/auth_verify.php / auth_verify | 生成统一 valid/type/reason 上下文;服务 Key 在标准链路触发 wrong_channel |
api/key/auth_hash.php / auth_hash | 集中定义认证哈希算法 |
api/key/keys.php / KeyStore::verify | 常驻有效期、服务 Key、一次性消费、独立 .lock 锁和 hash_equals |
api/key/keys.php / generateResident, fillPool, generateService, revoke, status | CLI Key 生命周期管理 |
api/key/base58.php / base58_encode, base58_decode | 32 字节随机 Key 的 Base58 编解码 |
一次性 Key 安全流
收到凭证
-> 对原文做 auth_hash
-> 锁定 keys.json.lock
-> 常驻 / 服务 / 一次性槽恒定时间比较
-> 一次性命中后把 key_hash 设为 null
-> safe write keys.json
-> 返回 type=onetime
-> 之后才进入路由业务校验这解释了为何已通过 Header 完成认证后,坏 JSON 或后续失败也会消费 Key。标准链路若仅把 Key 放在坏 JSON 的 body 中则无法提取凭证,也就不会消费。delete/list/stat 在 bootstrap 后显式禁止一次性 Key;create 允许;headless/get.php 当前未设置该禁令。
三、公共引导与配置
| 文件 | 职责 |
|---|---|
api/config.php | 域名、panel_origin、panel_enabled、时区、文件路径、普通/服务配额、服务短码长度、TTL 上限、保留字、内部 API 地址和令牌路径 |
api/common/bootstrap.php | 标准 JSON/CORS 头、面板开关、OPTIONS、数据访问检查、domain 检查、RAW_INPUT 缓存和认证 |
headless/bootstrap.php | 无头 JSON 头、OPTIONS、domain/数据检查、hl_error 和无头认证;不设置 CORS,不读取面板开关 |
api/common/helpers.php | 时间格式、过期判断、数据访问、KeyStore 单例、SSRF 和热同步重试 |
关键公共函数:
| 函数 | 用途 |
|---|---|
formatIso8601 | 支持固定偏移和 IANA 时区名称的 ISO 8601 输出 |
isExpired | 用 Unix 时间戳判断临时条目是否过期 |
cleanExpiredEntries | 写入流程中清理临时冷存储过期项 |
checkFileAccess / checkDataAccess | 文件存在时查可读写,不存在时查父目录可写 |
getKeyStore | 延迟创建共享 KeyStore 实例 |
isPrivateUrl | URL host、IP 字面量、DNS A/AAAA 的 SSRF 防护 |
internalSetWithRetry | 热写最多 3 次指数退避重试 |
hl_error | 无头 {error,message} 输出并终止请求 |
四、创建链路
阅读 api/routes/create.php 与 headless/create.php 时,按以下阶段定位:
- bootstrap 已完成数据访问检查与 Key 消费。
- 解码缓存的
RAW_INPUT。 - 校验 URL 字符串、HTTP(S)、2048 长度和 SSRF。
- 解析严格整数 TTL,缺省为 0。
- 根据
$AUTH['type']选择普通或服务短码/配额。 - 永久、无自定义短码时从冷存储做 URL 严格去重。
- 校验自定义短码与保留字。
JsonStore::withBothLocks同时锁定永久/临时 Store,原子检查冲突、配额并写入一个 Store。internalSetWithRetry同步 OpenResty 热存储。- 返回固定
short_url/exp/dedup/synced/warning/key_consumed字段。
无头创建的额外分支:服务 Key 可用 len 在 4/5 位自动短码间覆盖配置;自定义服务短码最长 5 位;创建使用 svc_perm_limit/svc_temp_limit。
安全相关代码锚点:
| 风险 | 入口 |
|---|---|
| URL 类型/长度 | 两个 create.php 的“验证目标链接”段 |
| SSRF | helpers.php::isPrivateUrl,由两个创建入口调用 |
| TTL 类型 | 两个 create.php 的 array_key_exists('ttl', ...) 分支 |
| 跨 Store TOCTOU | json_store.php::withBothLocks |
| 随机短码 | 两个 create.php 的 random_int 循环 |
| 总容量保护 | 两个 create.php 的四类 limit 总和检查 |
| 热同步窗口 | helpers.php::internalSetWithRetry 与 api/lua/internal.php |
五、删除、列表与统计
删除
api/routes/delete.php 与 headless/delete.php:
冷存储查类型
-> 对目标 Store 加独立 .lock 排他锁
-> 再读、再确认、删除、安全写入
-> internalDelete(code, "perm"|"temp")
-> {ok:true, synced:boolean}冷存储提交后不因热删除失败而回滚。Lua 使用 PHP 传入的冷存储类型作为计数器递减的权威提示。
列表与单条查询
| 文件 | 行为 |
|---|---|
api/routes/list.php | 冷存储列表,过滤过期临时项 |
headless/list.php | 同类行为,使用无头错误格式 |
headless/get.php | 永久优先,再查临时;过期返回 expired;条目结构损坏返回 data_corrupted |
统计
api/routes/stat.php 和 headless/stat.php 均先用 JsonStore::countActive() 计算冷存储权威值,再调用 internalStat() 获取热诊断值:
| 响应字段 | 来源 |
|---|---|
perm_count, temp_count | JSON 冷存储,有效条目遍历,权威 |
perm_limit, temp_limit | 普通配额配置 |
hot_available | 内部接口是否成功 |
hot_perm_count, hot_temp_count | su_meta 热计数,仅诊断且条件出现 |
标准 stat 还会记录热冷漂移日志;无头 stat 当前只在热不可用时记录日志。
六、冷存储
核心文件:api/storage/json_store.php。
| 函数 | 导航说明 |
|---|---|
read | 依赖原子 rename 的无锁完整读取;损坏时抛异常 |
write | 备份、.php.tmp、rename、读回验证、失败回滚 |
find, count, countActive | 查找与统计辅助 |
lockBegin, readLocked, writeLocked, lockEnd | 单 Store 原子读-检查-写 |
getPath | 为多 Store 固定锁序提供路径 |
withBothLocks | 同时锁永久和临时 Store,按路径排序避免死锁并消除跨 Store TOCTOU |
冷存储 envelope:
{
"v": 1,
"at": "ISO 8601",
"d": {
"code": { "id": "code", "url": "https://...", "lurl": "https://.../code", "t": "临时项才有" }
}
}Key 存储的安全写入在 api/key/keys.php::writeLockedFile,使用相同的备份、临时文件、rename、验证和回滚思想,但锁定 keys.json.lock。
七、PHP 到 OpenResty 的内部桥
核心文件:api/lua/internal.php。
| 函数 | 内部端点 | 说明 |
|---|---|---|
readInternalToken | - | 每次请求实时读取内部令牌,支持 CLI 轮换后立即生效 |
buildInternalHeaders | - | 构建 LPA-Key Header |
internalSet | POST /internal/set | 写 su_url/su_exp |
internalDelete | POST /internal/delete | 删除热条目并带类型提示 |
internalStat | GET /internal/stat | 读取热计数 |
internalPost | 内部 POST 通用 | cURL、Content-Type、HTTP 和 JSON 校验 |
getLastInternalError | - | 给 create/delete 记录同步诊断 |
内部接口同时依赖 Nginx IP allow/deny 和 LPA-Key。令牌文件路径由 api/config.php::internal_token_path 与 Nginx $su_internal_token_path 对齐。
八、OpenResty 热路径
| 文件 | 关键入口 | 职责 |
|---|---|---|
backend/lua/shorturl/internal.lua | dispatch | 验证内部令牌并分发 set/delete/stat |
internal_set.lua | handle | 二次验证 code/url/ttl/exp,写共享字典并维护计数 |
internal_delete.lua | handle | 删除共享字典,优先使用 PHP 类型提示递减计数 |
internal_stat.lua | handle | 从 su_meta 返回热计数 |
redirect.lua | go | 纯内存查 su_url 并按配置 301/302 跳转 |
init.lua | init_worker, init | worker 定时器注册、首次请求冷启动、共享字典重建 |
expire_sweep.lua | run 及恢复辅助 | 清理过期项、从冷存储恢复被驱逐项、重算计数 |
config.lua | 配置表 | init_worker 上下文无法读取 ngx.var 时的定时配置 |
util/time.lua | parse_iso8601, now_iso8601 等 | 与服务器本地时区无关的 ISO 8601 解析/输出 |
热存储结构:
| shared dict | 内容 |
|---|---|
su_url | code -> target URL,临时项使用原生 TTL |
su_exp | code -> "0" 或 ISO 8601,用于类型和过期恢复 |
su_meta | 初始化锁、路径、计数、驱逐标记、定时间隔 |
su_blocklist | 内部接口认证失败的临时阻断状态 |
九、部署与 CLI
| 文件 | 用途 |
|---|---|
nurl | 本机管理 Key、服务 Key、内部令牌、过期清理和状态 |
nurl-key | 通过无头 API 远程 list/stat/get/add/rm |
backend/nginx/nginx.md | 推荐主配置与站点路由 |
backend/nginx/nginx - 副本.md | 仓库中的配置副本;修改部署模板时应与主文件核对差异 |
SECURITY_FIXES.md | v1.10.6 到 v1.11.0 的安全修复背景与威胁说明 |
SECURITY_FIXES.md 建议与源码一起阅读,重点覆盖:Lua 随机播种移除、共享字典驱逐恢复、常驻 Key 验证只读化、双 Store TOCTOU 修复、损坏 Key 文件拒绝覆盖、跨服务器时区的 ISO 8601 修复。
十、按问题检索
| 想排查的问题 | 先看这些位置 |
|---|---|
| 为什么浏览器 CORS 失败 | api/config.php 的 panel_origin/panel_enabled、api/common/bootstrap.php、Nginx /api/ 的 limit_except |
| 为什么返回 430 | 两个 bootstrap 的 checkFileAccess 循环、服务器 error log |
| 为什么一次性 Key 失败后也没了 | KeyStore::verify,再看 bootstrap 与路由的执行顺序 |
| 为什么服务 Key 在标准 API 失败 | auth_verify.php 的 wrong_channel |
| 为什么服务短码能到 5 位 | headless/create.php 的 svcCodeLength/maxCodeLen/len、Nginx 1-5 位路由 |
| 为什么创建成功但暂时跳不通 | internalSetWithRetry、internal.php、成功响应的 synced/warning |
| 为什么删除成功但热缓存仍在 | delete 的冷先热后顺序、synced=false、internal_delete.lua |
| 为什么 stat 热冷不同 | PHP stat 的冷权威策略、internal_stat.lua、expire_sweep.lua |
| 为什么 URL 被判为内网 | helpers.php::isPrivateUrl 的 IP/DNS 双栈检查 |
| 为什么并发创建没有跨 Store 重码 | JsonStore::withBothLocks |
| 为什么 Nginx reload 后仍需恢复热数据 | init.lua::init_worker/init 与 expire_sweep.lua |