From 589f3582aed9d39f8ba5f698ae866b36880375e6 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:16:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20Pingora=20=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E7=9A=84=20SPA=20allowlist=20=E5=B9=B6=E5=85=B3?= =?UTF-8?q?=E9=97=AD=20SPA=20=E8=B7=AF=E7=94=B1=E6=BC=82=E7=A7=BB=E5=BE=85?= =?UTF-8?q?=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pingora MAIN_SPA_PATHS 从 5 条补到 12 条(补 /components、/design-system、/games、/games/detail、/games/mine、/games/play、/games/publish),与前端路由源和 nginx 三份模板逐条一致 - 路由对照矩阵新增 games_spa_fallback(/games/detail → static/web/spa_fallback)并同步 Pingora 试点文档的 SPA allowlist 表,让 cargo test -p pingora-gateway matches_nginx_route_parity_matrix 真正覆盖这条 - 门禁与验证:check:nginx-spa-routes OK(12 路由 / 3 模板)、check:pingora-route-parity OK(22 路由)、网关全量 38 passed、check:production-ops OK、cargo fmt --all --check OK;变异验证:临时拿掉 /games/detail 后矩阵用例以 static mode mismatch 变红;dev 与生产 5 条 /games* 深链实测 200 - 关闭【待办】主站SPA路由白名单与前端路由源不一致 并写入关闭记录;另立【待办】Pingora网关缺发行网关路由(三份 nginx 模板都有 /games/game_<32hex>/ → 发行网关,Pingora 缺这条代理,切流后会 404) --- deploy/pingora/nginx-route-parity.matrix.json | 20 +++++++++++++ ...…办】Pingora网关缺发行网关路由-2026-09-29.md | 28 +++++++++++++++++++ ...站SPA路由白名单与前端路由源不一致-2026-09-24.md | 16 +++++++++-- .../【待办】计划收口剩余项索引-2026-09-24.md | 4 ++- ...开发运维】Pingora独立网关试点-2026-06-11.md | 4 ++- server-rs/crates/pingora-gateway/src/main.rs | 19 ++++++++++++- 6 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 docs/project-memory/todos/【待办】Pingora网关缺发行网关路由-2026-09-29.md diff --git a/deploy/pingora/nginx-route-parity.matrix.json b/deploy/pingora/nginx-route-parity.matrix.json index f1c90ee98..be62874e2 100644 --- a/deploy/pingora/nginx-route-parity.matrix.json +++ b/deploy/pingora/nginx-route-parity.matrix.json @@ -297,6 +297,26 @@ }, "docs": ["主站 SPA allowlist", "失败回退 `/index.html`"] }, + { + "id": "games_spa_fallback", + "samplePath": "/games/detail", + "expect": { + "kind": "static", + "root": "web", + "mode": "spa_fallback" + }, + "nginx": { + "production": [ + "# BEGIN GENARRATIVE MAIN SPA ROUTES", + "try_files $uri /index.html =404;" + ], + "development": [ + "# BEGIN GENARRATIVE MAIN SPA ROUTES", + "try_files $uri /index.html =404;" + ] + }, + "docs": ["主站 SPA allowlist", "游戏目录 / 详情 / 游玩 / 我的 / 发布深链"] + }, { "id": "web_spa_case_trailing_slash", "samplePath": "/PROJECT/", diff --git a/docs/project-memory/todos/【待办】Pingora网关缺发行网关路由-2026-09-29.md b/docs/project-memory/todos/【待办】Pingora网关缺发行网关路由-2026-09-29.md new file mode 100644 index 000000000..e88ee3945 --- /dev/null +++ b/docs/project-memory/todos/【待办】Pingora网关缺发行网关路由-2026-09-29.md @@ -0,0 +1,28 @@ +# 【待办】Pingora 网关缺"发行网关"路由(Nginx 已有) + +更新时间:`2026-09-29` + +状态:开放。这是修「主站 SPA allowlist 漂移」时**顺手查出来的另一处漂移**:三份 Nginx 模板都把 `/games/game_<32 位十六进制 id>/…` 映射到发行网关,Pingora 网关没有对应分支。两个路由 parity 门禁现在都是绿的(`check:nginx-spa-routes` OK 12 路由、`check:pingora-route-parity` OK 22 路由),所以这条**不是**它们在报的问题,而是路由对照矩阵本身没覆盖的空白。 + +## 现象(2026-09-29 只读核对) + +- Nginx:`deploy/nginx/genarrative.conf:205`、`deploy/nginx/genarrative-dev-http.conf:193`、`deploy/container/nginx.conf:150` 都有 + `location ~ "^/games/(?game_[0-9a-f]{32})(?/.*)?$"`, + 内部 `proxy_set_header Cookie "";` + `proxy_pass http://genarrative_api/api/game-distribution/releases/$game_id$game_path;`。 +- Pingora:`server-rs/crates/pingora-gateway/src/main.rs` 的 `classify_path` 没有任何 `/games/...` 分支(`MAIN_SPA_PATHS` 里也**不应该**有它),于是落到末尾的 `Static { root: Web, mode: Exact }` —— 真机上就是 404。 +- 矩阵:`deploy/pingora/nginx-route-parity.matrix.json` 没有这条用例,所以 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 覆盖不到。 + +## 影响 + +Pingora 目前只监听 `127.0.0.1:18081`(shadow / direct 试点),公网入口仍是 Nginx,**当前没有用户可见故障**。但试点文档写明「切流前必须与 Nginx 逐条对齐」;一旦把公网入口切到 Pingora,**所有已公开游戏的在线游玩入口 `/games/game_/` 会 404**,而目录与详情仍然 200 —— 必须从详情页点「立即玩」才会暴露,属于很难第一时间发现的缺口。 + +## 修法建议 + +1. 网关加一个独立的"发行网关代理"决策(不要塞进 SPA allowlist):匹配 `^/games/game_[0-9a-f]{32}(?:/.*)?$`,代理到 api 上游的 `/api/game-distribution/releases/`,转发时**清空 Cookie**(与 Nginx 同口径:发行内容不读账号凭证),其余响应头(CSP / nosniff / CORP)仍由 api-server 出。 +2. 矩阵补 `games_release_gateway` 用例并把三份模板的同名片段写进 `nginx` 字段;因为这是「路径重写 + 清 Cookie」,需要给矩阵加一种表达(新增 `expect.kind`,或给 `proxy` 增加可选的重写/头字段),让 `check:pingora-route-parity` 能逐条比对。 +3. 验证:`cargo test -p pingora-gateway matches_nginx_route_parity_matrix` + `check:pingora-gateway-smoke`(本机需先设 `OPENSSL_CONF`,见 pitfalls);再加一条反例断言 `/games/not-a-game-id/` 仍是真实 404。 + +## 关闭条件 + +- 矩阵里有该路由的用例,`npm run check:pingora-route-parity` 与 `npm run check:nginx-spa-routes` 都绿,`cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 逐条断言通过。 +- `check:pingora-gateway-smoke` 在真实网关进程上证明 `/games/game_<32hex>/…` 被代理到发行网关、且上游拿不到 Cookie;`/games/not-a-game-id/` 仍返回 404。 diff --git a/docs/project-memory/todos/【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md b/docs/project-memory/todos/【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md index 359212d08..83d217d6e 100644 --- a/docs/project-memory/todos/【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md +++ b/docs/project-memory/todos/【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md @@ -2,7 +2,7 @@ 更新时间:`2026-09-24` -状态:部分关闭。共享组件展示页(`/components`、`/design-system`)的缺失已在本轮补齐;剩余 5 条 `/games*` 路由属冻结的游戏分发系列,`npm run check:nginx-spa-routes` 与 `npm run check:pingora-route-parity` 仍为红灯。 +状态:**已关闭(2026-09-29)**。两个门禁都已转绿,关闭证据见文末「关闭记录(2026-09-29)」。 ## 现象(2026-09-24 实跑) @@ -39,4 +39,16 @@ 1. `npm run check:nginx-spa-routes` 与 `npm run check:pingora-route-parity` 通过,三份模板的 allowlist 集合与前端路由源逐条一致。 2. 大小写与尾部斜杠容忍、`/creation/not-exist` 等未知路径仍返回 404 的判据不被放宽。 -3. 游戏部分补齐前,本文件保持打开,不能把「门禁仍红」当成已收口。 \ No newline at end of file +3. 游戏部分补齐前,本文件保持打开,不能把「门禁仍红」当成已收口。 + +## 关闭记录(2026-09-29) + +三条关闭条件逐条复验通过: + +1. **两个门禁都绿**:`npm run check:nginx-spa-routes` → `OK (12 SPA routes, 3 Nginx templates)`;`npm run check:pingora-route-parity` → `OK (22 routes)`。nginx 侧那 5 条 `/games*` 是随 `87e52860a`(游戏发行入口改为平台同源路径)补进三份模板的;这轮把**漏掉的 Pingora 侧**补齐——`server-rs/crates/pingora-gateway/src/main.rs` 的 `MAIN_SPA_PATHS` 从 5 条补到 12 条(`/components`、`/design-system`、`/games`、`/games/detail`、`/games/mine`、`/games/play`、`/games/publish` 与原有 5 条),并同步 `docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md` 的 SPA allowlist 表。 +2. **判据没有被放宽**:`check-nginx-spa-routes.mjs` 仍在断言大小写不敏感、允许一个尾部斜杠、`/creation/not-exist` 等未知路径必须只读真实静态文件并返回 404;`is_main_spa_path()` 仍是精确(大小写不敏感 + 去一个尾部斜杠)匹配,不做前缀匹配,所以 `/games/game_<32hex>/` 不会被误当成 SPA。 +3. **运行时口径**:`dev.genarrative.world` 与 `genarrative.world` 的 `/games`、`/games/detail?id=1`、`/games/play?id=1`、`/games/mine`、`/games/publish` 实测都返回 200 + SPA 外壳(`陶泥儿 Genarrative|美术编辑器与项目工作台`)。 + +验证证据:`deploy/pingora/nginx-route-parity.matrix.json` 新增 `games_spa_fallback`(`samplePath=/games/detail`,期望 `static/web/spa_fallback`),`cargo test -p pingora-gateway matches_nginx_route_parity_matrix` → **1 passed**;**变异验证**:临时从 `MAIN_SPA_PATHS` 拿掉 `"/games/detail"` 后该用例立刻失败(`route parity static mode mismatch: games_spa_fallback /games/detail,left: Some("exact") right: Some("spa_fallback")`),`check:pingora-route-parity` 同时报出缺路由。网关全量单测 `cargo test -p pingora-gateway` → **38 passed**;`npm run check:production-ops` 通过;`cargo fmt --all -- --check` 通过。 + +**顺带发现并另立待办**:三份 Nginx 模板都有 `/games/game_<32hex>/…` → 发行网关的代理(清 Cookie),Pingora 没有对应分支,切流后会 404。见 [`【待办】Pingora网关缺发行网关路由-2026-09-29.md`](【待办】Pingora网关缺发行网关路由-2026-09-29.md)。 diff --git a/docs/project-memory/todos/【待办】计划收口剩余项索引-2026-09-24.md b/docs/project-memory/todos/【待办】计划收口剩余项索引-2026-09-24.md index edbe8316d..c6f980b69 100644 --- a/docs/project-memory/todos/【待办】计划收口剩余项索引-2026-09-24.md +++ b/docs/project-memory/todos/【待办】计划收口剩余项索引-2026-09-24.md @@ -46,10 +46,12 @@ | `【待办】引用输入区后续收口-2026-09-24.md` | CSS 类名改名一条已按现状关闭(接受不改名);剩「归一化撞名是否提示」与真机手感验收 | 产品决定 + 真机 | | `【待办】画布验收后续修复-2026-09-18.md` | 三批修复与三项画布后续需求已落地本地;动画生成失败的真实复现、多选卡顿的现场 Profiler、以及全部真机观感仍未取证 | 真实客户端 + 现场 | | `【目标编辑器适配】Unity与Unreal项目识别与导入-2026-09-11.md` | 明确「暂不实现」的新功能 | 排期决定 | -| `【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md` | `check:nginx-spa-routes` 自 2026-08-26 起红灯;2026-09-24 已补回 `/components`、`/design-system`(失败清单从 7 条降到 5 条),剩 5 条 `/games*`;CI 与 `npm run lint` 都不含该门禁 | 剩游戏部分:属冻结的游戏分发系列,等阶段 A 验收结论 | +| `【待办】Pingora网关缺发行网关路由-2026-09-29.md`(新增) | 三份 Nginx 模板都有 `/games/game_<32hex>/…` → 发行网关(清 Cookie),Pingora 的 `classify_path` 没有对应分支,切流后会 404;两个路由 parity 门禁与矩阵都覆盖不到这一条。当前 Pingora 只在 `127.0.0.1:18081` shadow,无用户可见故障 | 归 Pingora 试点切流前的对齐清单 | ## 五、已收口口径(供复核) +- **已关闭(2026-09-29)**:`【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md`。nginx 侧 5 条 `/games*` 随 `87e52860a` 补齐;这轮补上漏掉的 Pingora 侧(`MAIN_SPA_PATHS` 5→12 条 + 矩阵新增 `games_spa_fallback` 用例 + 文档表同步)。`check:nginx-spa-routes` OK(12 路由 / 3 模板)、`check:pingora-route-parity` OK(22 路由)、`cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 1 passed(变异验证:拿掉 `/games/detail` 立刻变红)、网关全量 38 passed;dev 与生产上 `/games`、`/games/detail`、`/games/play`、`/games/mine`、`/games/publish` 实测 200 + SPA 外壳。该文件已在正文写明关闭记录,闭项从第四节移到本条。 + - 74 份计划里 63 份为 `implemented-awaiting-runtime-acceptance`(含用括号写清边界的那批,含 `- Status:` 与表格两种写法);其余 11 份全部落在本文件一、二、三节里——这三节另外还列了 10 份状态已是 `implemented-awaiting-runtime-acceptance`、但仍有条目未勾选或未验收的计划,所以三节合计 21 份,不要与本条那 11 份混算。 - 未勾选复选框从 85 条降到 43 条(2026-09-24 复算口径:`- [ ]` 行首复选框,74 份计划合计):本轮把已有本地证据的条目逐条勾选,并在每份计划里写了本轮的勾选依据或复验记录——多数是「逐条勾选依据(2026-09-24)」章节,另有「本轮复验(2026-09-24)」「本轮复核(2026-09-24)」写法,`【里程碑】DirectProject聊天真相源收敛-2026-09-16.md` 则把自动化证据直接写在条目里(`【自动化:…】`);仍勾不动的条目都能在依据里读到具体原因。 - 仍为红灯的门禁(2026-09-24 实跑,与计划条目无关但需知晓):`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`(`/components`、`/design-system` 已于本日补回,剩 5 条 `/games*` 的 SPA allowlist;详见 `【待办】主站SPA路由白名单与前端路由源不一致-2026-09-24.md`);两者都不在 CI 与 `npm run lint` 的覆盖范围内。 diff --git a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md index d5d5e3f34..a5a83e367 100644 --- a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md +++ b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md @@ -534,9 +534,11 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清 | `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 | | `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 | | `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 | -| 主站 SPA allowlist | 只对 `/`、`/creation`、`/project`、`/profile` 与 `/editor/canvas` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。 | +| 主站 SPA allowlist | 只对 `/`、`/components`、`/creation`、`/design-system`、`/editor/canvas`、`/games`、`/games/detail`、`/games/mine`、`/games/play`、`/games/publish`、`/profile`、`/project` 失败回退 `/index.html`(集合与前端路由源、Nginx 三份模板逐条一致,由 `npm run check:pingora-route-parity` 与 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 比对);匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。`/games/game_<32 位十六进制 id>/…` 是发行网关路由,不在 SPA allowlist 内。 | | 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 不进入 SPA fallback。 | +SPA allowlist 里属于游戏分发入口的深链(游戏目录 / 详情 / 游玩 / 我的 / 发布深链:`/games`、`/games/detail`、`/games/play`、`/games/mine`、`/games/publish`)与 Nginx 三份模板同口径;Pingora 侧由路由对照矩阵的 `games_spa_fallback` 用例与 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 逐条断言。发行网关路径 `/games/game_<32 位十六进制 id>/…` 不走 SPA,见下一节的对照说明。 + 维护模式下,公网 API-like 路由返回 JSON `503`;公网 Web 静态路由先读取 `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_PAGE_FILE` 指向的 release 外运行态公告,缺失时回退 `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT/maintenance.html`,两者都不存在时返回纯文本 `503`。版本化默认页不得包含日期或具体时段,临时公告由 `maintenance-on.sh --page-file` 安装并在 `maintenance-off.sh` 时清理。IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local 来源绕过整站维护闸,主站页面与静态资源、普通 API、后台页面与后台 API、SpacetimeDB 路由均按非维护状态继续处理;应用层登录、管理员鉴权和其它业务鉴权保持不变。Pingora 直连按 TCP peer 判定来源;仅当 peer 是 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的 `X-Real-IP`,绝不使用客户端可伪造的 `X-Forwarded-For` 做维护放行。该放行只绕过网关维护响应;若 `pause-after-stdb` 已停止 api-server,内网普通 API 和后台 API 仍不可用。 代理失败时,API / SpacetimeDB 等代理路由返回统一 JSON 网关错误;本地静态路由仍保持对应 HTTP 错误状态。 静态 `Range` 只支持单段 bytes range;多段 range 暂按完整文件返回,避免在正式替换前引入 multipart 响应面。`If-None-Match` / `If-Modified-Since` 优先于 `Range` 判定,命中时仍返回 `304`;`If-Range` 日期匹配时继续返回 `206`,日期旧于文件或弱 ETag 校验器时回完整 `200`;`206` / `304` / `416` 不做 gzip 压缩,避免 `Content-Range` 语义被响应体改写破坏。Gateway smoke 会用固定 `X-Request-Id` 对账静态 `304`、`405`、`206`、`416` 的 Pingora access log 行,确认本地响应状态也进入正式切换证据链。 diff --git a/server-rs/crates/pingora-gateway/src/main.rs b/server-rs/crates/pingora-gateway/src/main.rs index 9c1a6e877..4172b143e 100644 --- a/server-rs/crates/pingora-gateway/src/main.rs +++ b/server-rs/crates/pingora-gateway/src/main.rs @@ -57,7 +57,24 @@ const SHADOW_PROBE_HEADER: &str = "x-genarrative-pingora-probe"; const PAYLOAD_TOO_LARGE_CONTEXT: &str = "genarrative_payload_too_large"; const PROTECTION_STATE_TTL: Duration = Duration::from_secs(600); const PROTECTION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); -const MAIN_SPA_PATHS: &[&str] = &["/", "/creation", "/editor/canvas", "/profile", "/project"]; +// 必须与前端路由源(`src/routing/activeAppPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` 与 +// `src/routing/activeAppRoutes.tsx`)以及 nginx 三份模板的 SPA allowlist 逐条一致: +// 门禁 `npm run check:pingora-route-parity` 会逐条比对,漏一条就等于把该深链在 +// Pingora 网关下打成 404(`/games/game_/` 是发行网关路由,不在这个 allowlist 里)。 +const MAIN_SPA_PATHS: &[&str] = &[ + "/", + "/components", + "/creation", + "/design-system", + "/editor/canvas", + "/games", + "/games/detail", + "/games/mine", + "/games/play", + "/games/publish", + "/profile", + "/project", +]; #[derive(Clone, Debug)] struct GatewayConfig {