diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 2044455fd..53b8a753d 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1728,3 +1728,11 @@ - 影响范围:`shared-contracts` 默认 spec、`module-runtime` 入口配置响应、`spacetime-module` 后台保存校验、后台入口开关页摘要和前端 fallback spec。 - 验证方式:`GET /api/creation-entry/config` 中各玩法 `unifiedCreationSpec.title` 等于已保存契约内容;后台只修改入口名称时不应隐式改写已保存的统一创作页表头。 - 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-06-11 Pingora 网关先独立二进制试点 + +- 背景:评估 Pingora 是否逐步替代当前生产 Nginx 时,需要先验证 Genarrative 的现有反向代理、静态资源、维护模式和最小 SpacetimeDB 公网路由口径。 +- 决策:新增 `server-rs/crates/pingora-gateway` 作为独立 binary crate,默认监听 `127.0.0.1:18081` 做影子网关;当前不绑定 `80/443`,不替代 `deploy/nginx/genarrative.conf`,生产仍以 Nginx 为公网入口。 +- 影响范围:Rust workspace、Pingora 依赖、网关环境变量、`deploy/pingora/pingora-gateway.env.example` 和运维技术方案。 +- 验证方式:先执行 `cargo fmt --manifest-path server-rs/Cargo.toml -p pingora-gateway`、`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml`;替换前必须补齐路由 parity、压缩、TLS、限流、systemd、Jenkins 和健康巡检。 +- 关联文档:`docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md`。 diff --git a/deploy/pingora/pingora-gateway.env.example b/deploy/pingora/pingora-gateway.env.example new file mode 100644 index 000000000..d53174ae8 --- /dev/null +++ b/deploy/pingora/pingora-gateway.env.example @@ -0,0 +1,16 @@ +# Pingora 独立网关试点配置示例。 +# 当前只用于本机影子验证,不绑定 80/443,也不替代生产 Nginx。 + +GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081 +GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM=127.0.0.1:8082 +GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM=127.0.0.1:3101 + +GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT=/srv/genarrative/web +GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT=/var/www/html +GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE=/var/lib/genarrative/maintenance/enabled + +GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=http +GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES=67108864 + +GENARRATIVE_PINGORA_GATEWAY_LOG=info,pingora=info,pingora_gateway=info +GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED=false diff --git a/docs/README.md b/docs/README.md index 00c2f8431..37cb061cd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ 微信小程序虚拟支付接入、`wechat_mp_virtual` 渠道、`wx.requestVirtualPayment` 承接页和后端签名配置见 [【技术方案】微信虚拟支付接入-2026-05-26.md](./%E3%80%90%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88%E3%80%91%E5%BE%AE%E4%BF%A1%E8%99%9A%E6%8B%9F%E6%94%AF%E4%BB%98%E6%8E%A5%E5%85%A5-2026-05-26.md)。 -生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。 +生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。Pingora 只作为独立二进制影子网关试点时,边界、路由口径与替换前验收见 [【开发运维】Pingora独立网关试点-2026-06-11.md](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。 SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。 diff --git a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md new file mode 100644 index 000000000..c58a39de3 --- /dev/null +++ b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md @@ -0,0 +1,66 @@ +# Pingora 独立网关试点 + +## 目标 + +本试点新增 `server-rs/crates/pingora-gateway` 独立二进制 crate,用 Pingora 复刻当前生产 Nginx 的核心反向代理与静态路由口径。当前阶段只作为影子网关验证,不绑定公网 `80/443`,不替代 `deploy/nginx/genarrative.conf`。 + +## 运行边界 + +- 默认监听 `127.0.0.1:18081`,只用于本机或容器内 smoke。 +- 默认转发 `api-server` 到 `127.0.0.1:8082`。 +- 默认转发最小 SpacetimeDB 公网路由到 `127.0.0.1:3101`。 +- 默认静态目录为 `/srv/genarrative/web`,维护开关文件为 `/var/lib/genarrative/maintenance/enabled`。 +- 当前不承接 TLS、HTTP 到 HTTPS 跳转、gzip/Brotli、完整连接数限制、完整 RPS 限流、Certbot ACME 自动化、Jenkins 产物发布和 systemd 健康巡检。 + +## 启动命令 + +本机影子验证时先启动 SpacetimeDB 与 `api-server`,再运行: + +```bash +GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT=/srv/genarrative/web \ +cargo run -p pingora-gateway --manifest-path server-rs/Cargo.toml +``` + +也可以复制 `deploy/pingora/pingora-gateway.env.example` 到部署环境的非 Git 配置文件,由 systemd 或容器注入。 + +## 环境变量 + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `GENARRATIVE_PINGORA_GATEWAY_LISTEN` | `127.0.0.1:18081` | Pingora 监听地址。 | +| `GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM` | `127.0.0.1:8082` | `api-server` 上游地址。 | +| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM` | `127.0.0.1:3101` | SpacetimeDB 上游地址。 | +| `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT` | `/srv/genarrative/web` | 前端静态文件根目录。 | +| `GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT` | `/var/www/html` | ACME challenge 静态目录。 | +| `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE` | `/var/lib/genarrative/maintenance/enabled` | 存在即进入维护模式。 | +| `GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO` | `http` | 写入 `X-Forwarded-Proto` 的值。 | +| `GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES` | `67108864` | `/api` 通用路由的 `Content-Length` 上限。 | +| `GENARRATIVE_PINGORA_GATEWAY_LOG` | `info,pingora=info,pingora_gateway=info` | tracing 过滤器。 | +| `GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED` | `false` | 是否启用共享 OpenTelemetry 初始化。 | + +## 当前路由口径 + +| 路由 | 行为 | +| --- | --- | +| `/.well-known/acme-challenge/*` | 从 `GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT` 精确读取静态文件。 | +| `/admin` | 301 到 `/admin/`。 | +| `/admin/api/*` | 转发到 `api-server`。 | +| `/admin/assets/*` | 从 Web 根目录精确读取静态文件。 | +| `/admin/*` | 先读取静态文件或目录 index,失败回退 `/admin/index.html`。 | +| `/assets/*` | 从 Web 根目录精确读取静态文件。 | +| `/api/runtime/puzzle/gallery`、`/api/runtime/custom-world-gallery` | 转发到 `api-server`。 | +| `/api/runtime/puzzle/gallery/{id}`、`/api/runtime/custom-world-gallery/{profile}/{owner}` | 转发到 `api-server`。 | +| `/api`、`/api/*` | 转发到 `api-server`,按配置执行 `Content-Length` 上限检查。 | +| `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 | +| `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 | +| 其它路径 | 先读取静态文件或目录 index,失败回退 `/index.html`。 | + +维护模式下,API-like 路由返回 JSON `503`,Web 静态路由优先返回 `maintenance.html`,不存在时返回纯文本 `503`。 + +## 后续替换前验收 + +1. 容器内使用同一份 Web 产物、同一组上游地址跑 Pingora smoke。 +2. 对照 `deploy/nginx/genarrative.conf` 做路由 parity 自动测试,覆盖 API、Admin、SPA fallback、静态资源、维护模式和 SpacetimeDB WebSocket。 +3. 补齐限流、连接数限制、压缩、TLS、ACME 和访问日志字段。 +4. 增加 systemd service 模板与健康巡检,但仍由 Nginx 反向代理到 Pingora 做 canary。 +5. canary 稳定后,再评估是否让 Pingora 直接承接公网入口。 diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index f285faaf3..aa52b22d9 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -35,6 +44,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -64,6 +74,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -73,6 +89,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -216,7 +282,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -227,7 +293,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -297,6 +363,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.21.7" @@ -398,6 +479,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -410,6 +502,12 @@ version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -462,6 +560,34 @@ dependencies = [ "shlex", ] +[[package]] +name = "cf-rustracing" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f85c3824e4191621dec0551e3cef3d511f329da9a8990bf3e450a85651d97e" +dependencies = [ + "backtrace", + "rand 0.8.6", + "tokio", + "trackable", +] + +[[package]] +name = "cf-rustracing-jaeger" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a5f80d44c257c3300a7f45ada676c211e64bbbac591bbec19344a8f61fbcab" +dependencies = [ + "cf-rustracing", + "hostname", + "local-ip-address", + "percent-encoding", + "rand 0.9.4", + "thrift_codec", + "tokio", + "trackable", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -498,6 +624,61 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "console" version = "0.16.3" @@ -665,7 +846,35 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + +[[package]] +name = "daggy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70def8d72740e44d9f676d8dab2c933a236663d86dd24319b57a2bed4d694774" +dependencies = [ + "petgraph 0.7.1", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -674,8 +883,22 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] @@ -688,7 +911,18 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", ] [[package]] @@ -697,9 +931,9 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -728,6 +962,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -736,7 +981,38 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", ] [[package]] @@ -749,7 +1025,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.117", ] [[package]] @@ -771,7 +1047,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -816,7 +1092,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -836,7 +1112,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -852,7 +1128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -900,6 +1176,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -907,6 +1189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", + "libz-ng-sys", "miniz_oxide", ] @@ -922,6 +1205,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1002,7 +1291,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1084,6 +1373,24 @@ dependencies = [ "wasip3", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -1109,6 +1416,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1121,7 +1447,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1130,7 +1456,9 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", "equivalent", + "foldhash 0.2.0", "rayon", "serde", "serde_core", @@ -1141,6 +1469,11 @@ name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1178,6 +1511,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "http" version = "0.2.12" @@ -1261,7 +1605,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1342,7 +1686,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1578,6 +1922,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.12.1" @@ -1700,6 +2050,16 @@ dependencies = [ "glob", ] +[[package]] +name = "libz-ng-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879917b256f6317769b9f374b435805a8697013098aacce5a38ac106cd6a9469" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "libz-sys" version = "1.1.29" @@ -1724,6 +2084,17 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "local-ip-address" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa08fb2b1ec3ea84575e94b489d06d4ce0cbf052d12acd515838f50e3c3d63e3" +dependencies = [ + "libc", + "neli", + "windows-sys 0.61.2", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -1757,6 +2128,15 @@ dependencies = [ "weezl", ] +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1794,6 +2174,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2091,6 +2480,47 @@ dependencies = [ "tempfile", ] +[[package]] +name = "neli" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" +dependencies = [ + "bitflags 2.11.1", + "byteorder", + "derive_builder", + "getset", + "libc", + "log", + "neli-proc-macros", + "parking_lot", +] + +[[package]] +name = "neli-proc-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -2113,7 +2543,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2150,12 +2580,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl" version = "0.10.78" @@ -2179,7 +2624,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2361,7 +2806,17 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap 2.14.0", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", "indexmap 2.14.0", ] @@ -2382,7 +2837,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2391,6 +2846,230 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pingora" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a75f2ff8e122aa80ab202dc865294fe59cd856c2a5dab2d3df6e122c93b941" +dependencies = [ + "pingora-core", + "pingora-http", + "pingora-proxy", + "pingora-timeout", +] + +[[package]] +name = "pingora-cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527735ac204efb9fa3884bfd9224d016c5735fabe1d394ebed145b40e7545b99" +dependencies = [ + "ahash", + "async-trait", + "blake2", + "bstr", + "bytes", + "cf-rustracing", + "cf-rustracing-jaeger", + "hex", + "http 1.4.0", + "httparse", + "httpdate", + "indexmap 1.9.3", + "log", + "lru", + "once_cell", + "parking_lot", + "pingora-core", + "pingora-error", + "pingora-header-serde", + "pingora-http", + "pingora-lru", + "pingora-timeout", + "rand 0.8.6", + "regex", + "rmp", + "rmp-serde", + "serde", + "strum 0.26.3", + "tokio", +] + +[[package]] +name = "pingora-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a7ffe2f5acf9f94fd255cfd1438866bc9124f8f0c7d42562bd3f853df2094b7" +dependencies = [ + "ahash", + "async-trait", + "brotli", + "bstr", + "bytes", + "chrono", + "clap", + "daemonize", + "daggy", + "derivative", + "flate2", + "futures", + "h2 0.4.14", + "http 1.4.0", + "httparse", + "httpdate", + "libc", + "log", + "nix", + "once_cell", + "openssl-probe", + "parking_lot", + "percent-encoding", + "pingora-error", + "pingora-http", + "pingora-pool", + "pingora-runtime", + "pingora-timeout", + "prometheus 0.13.4", + "rand 0.8.6", + "regex", + "serde", + "serde_yaml", + "sfv", + "socket2 0.6.3", + "strum 0.26.3", + "strum_macros 0.26.4", + "tokio", + "tokio-stream", + "tokio-test", + "unicase", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "pingora-error" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23f7bc013de67e44ed902a82843f6157460b89d11da882bcc6f09f8ae380af1" + +[[package]] +name = "pingora-gateway" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "mime_guess", + "pingora", + "pingora-http", + "pingora-proxy", + "shared-logging", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "pingora-header-serde" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828c0e53e74160cbfe8e67dd3a811eb6a253c36acbaf7a39a01d9aacfb9ac139" +dependencies = [ + "bytes", + "http 1.4.0", + "httparse", + "pingora-error", + "pingora-http", + "thread_local", + "zstd", + "zstd-safe", +] + +[[package]] +name = "pingora-http" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d553d310a15ec88107b9388a02885f798efc57764d8e9bdaaf32a76722927a10" +dependencies = [ + "bytes", + "http 1.4.0", + "pingora-error", +] + +[[package]] +name = "pingora-lru" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6705a26ad89d241a989a5395641931ba37076f5ab5fbd19ee92402414a43af32" +dependencies = [ + "arrayvec", + "hashbrown 0.17.0", + "parking_lot", + "rand 0.8.6", +] + +[[package]] +name = "pingora-pool" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb1237893b15a9cf6b371bee8d7e2e1c10742e4be6eb00ed38cfe87fd1363f8" +dependencies = [ + "crossbeam-queue", + "log", + "lru", + "parking_lot", + "pingora-timeout", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-proxy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a92ee756ecf6ecb6419864da651cad6cecd933b6d420a26877031efa16bef57" +dependencies = [ + "async-trait", + "bytes", + "clap", + "futures", + "h2 0.4.14", + "http 1.4.0", + "log", + "once_cell", + "pingora-cache", + "pingora-core", + "pingora-error", + "pingora-http", + "rand 0.8.6", + "regex", + "tokio", +] + +[[package]] +name = "pingora-runtime" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41815a13691a3e7d9ad0e34767d4140284132e31b95a4481f5e73ab6f407f834" +dependencies = [ + "once_cell", + "rand 0.8.6", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-timeout" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e321452eaa461e0b6c5aaa35b7e42527ee89df33710279f37fae7f066b68e" +dependencies = [ + "once_cell", + "parking_lot", + "pin-project-lite", + "thread_local", + "tokio", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2586,7 +3265,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2598,6 +3299,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf 2.28.0", + "thiserror 1.0.69", +] + [[package]] name = "prometheus" version = "0.14.0" @@ -2609,7 +3325,7 @@ dependencies = [ "lazy_static", "memchr", "parking_lot", - "protobuf", + "protobuf 3.7.2", "thiserror 2.0.18", ] @@ -2633,9 +3349,15 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "protobuf" version = "3.7.2" @@ -2681,7 +3403,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2718,7 +3440,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.59.0", ] @@ -2855,7 +3577,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2898,7 +3620,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -2987,6 +3709,41 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rust_decimal" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3012,7 +3769,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3125,7 +3882,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -3202,7 +3959,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3213,7 +3970,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3286,10 +4043,34 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sfv" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "rust_decimal", ] [[package]] @@ -3534,7 +4315,7 @@ dependencies = [ "proc-macro2", "quote", "spacetimedb-primitives", - "syn", + "syn 2.0.117", ] [[package]] @@ -3564,7 +4345,7 @@ dependencies = [ "spacetimedb-lib", "spacetimedb-primitives", "spacetimedb-sats", - "strum", + "strum 0.25.0", "thiserror 1.0.69", ] @@ -3627,7 +4408,7 @@ dependencies = [ "arrayvec", "itertools", "paste", - "prometheus", + "prometheus 0.14.0", ] [[package]] @@ -3702,7 +4483,7 @@ dependencies = [ "itertools", "lazy_static", "lean_string", - "petgraph", + "petgraph 0.6.5", "serde_json", "smallvec", "spacetimedb-data-structures", @@ -3735,7 +4516,7 @@ dependencies = [ "log", "native-tls", "once_cell", - "prometheus", + "prometheus 0.14.0", "rand 0.9.4", "spacetimedb-client-api-messages", "spacetimedb-data-structures", @@ -3788,7 +4569,16 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros", + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", ] [[package]] @@ -3801,7 +4591,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", ] [[package]] @@ -3810,6 +4613,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -3844,7 +4658,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3878,7 +4692,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3920,7 +4734,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3931,7 +4745,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3943,6 +4757,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "thrift_codec" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d957f535b242b91aa9f47bde08080f9a6fef276477e55b0079979d002759d5" +dependencies = [ + "byteorder", + "trackable", +] + [[package]] name = "time" version = "0.3.47" @@ -4024,7 +4848,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4058,6 +4882,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-tungstenite" version = "0.27.0" @@ -4239,7 +5074,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4297,6 +5132,25 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4388,6 +5242,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -4424,6 +5284,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -4529,7 +5395,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -4649,7 +5515,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -4673,7 +5539,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4684,7 +5550,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4923,7 +5789,7 @@ dependencies = [ "heck 0.5.0", "indexmap 2.14.0", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4939,7 +5805,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5006,7 +5872,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -5027,7 +5893,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5047,7 +5913,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -5087,7 +5953,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5125,6 +5991,34 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.5.1" diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index cdc461bdf..953865a93 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -40,6 +40,7 @@ members = [ "crates/platform-wechat", "crates/platform-speech", "crates/platform-agent", + "crates/pingora-gateway", "crates/shared-contracts", "crates/shared-kernel", "crates/shared-logging", @@ -87,6 +88,7 @@ platform-llm = { path = "crates/platform-llm", default-features = false } platform-oss = { path = "crates/platform-oss", default-features = false } platform-speech = { path = "crates/platform-speech", default-features = false } platform-wechat = { path = "crates/platform-wechat", default-features = false } +pingora-gateway = { path = "crates/pingora-gateway", default-features = false } shared-contracts = { path = "crates/shared-contracts", default-features = false } shared-kernel = { path = "crates/shared-kernel", default-features = false } shared-logging = { path = "crates/shared-logging", default-features = false } @@ -111,6 +113,10 @@ image = { version = "0.25", default-features = false } jsonwebtoken = "9" langchainrust = "0.2.18" log = "0.4" +mime_guess = "2.0.5" +pingora = { version = "0.8.1", default-features = false, features = ["proxy"] } +pingora-http = { version = "0.8.1", default-features = false } +pingora-proxy = { version = "0.8.1", default-features = false } rand_core = "0.6" reqwest = { version = "0.12", default-features = false } ring = "0.17" diff --git a/server-rs/crates/pingora-gateway/Cargo.toml b/server-rs/crates/pingora-gateway/Cargo.toml new file mode 100644 index 000000000..c4602aaf2 --- /dev/null +++ b/server-rs/crates/pingora-gateway/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pingora-gateway" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +bytes = { workspace = true } +mime_guess = { workspace = true } +pingora = { workspace = true } +pingora-http = { workspace = true } +pingora-proxy = { workspace = true } +shared-logging = { workspace = true } +tokio = { workspace = true, features = ["fs"] } +tracing = { workspace = true } +uuid = { workspace = true, features = ["v4"] } diff --git a/server-rs/crates/pingora-gateway/src/main.rs b/server-rs/crates/pingora-gateway/src/main.rs new file mode 100644 index 000000000..45757fcdc --- /dev/null +++ b/server-rs/crates/pingora-gateway/src/main.rs @@ -0,0 +1,818 @@ +use std::{ + env, io, + net::SocketAddr, + path::{Component, Path, PathBuf}, + sync::Arc, + time::Instant, +}; + +use async_trait::async_trait; +use bytes::Bytes; +use pingora::{ + Result as PingoraResult, + prelude::HttpPeer, + server::{Server, configuration::Opt}, +}; +use pingora_http::{Method, ResponseHeader}; +use pingora_proxy::{ProxyHttp, Session, http_proxy_service}; +use shared_logging::{OtelConfig, init_tracing}; +use tracing::{error, info, warn}; +use uuid::Uuid; + +const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:18081"; +const DEFAULT_API_UPSTREAM: &str = "127.0.0.1:8082"; +const DEFAULT_SPACETIME_UPSTREAM: &str = "127.0.0.1:3101"; +const DEFAULT_WEB_ROOT: &str = "/srv/genarrative/web"; +const DEFAULT_ACME_ROOT: &str = "/var/www/html"; +const DEFAULT_MAINTENANCE_FILE: &str = "/var/lib/genarrative/maintenance/enabled"; +const DEFAULT_MAX_API_BODY_BYTES: u64 = 64 * 1024 * 1024; +const DEFAULT_LOG_FILTER: &str = "info,pingora=info,pingora_gateway=info"; + +#[derive(Clone, Debug)] +struct GatewayConfig { + listen_addr: String, + api_upstream: SocketAddr, + spacetime_upstream: SocketAddr, + web_root: PathBuf, + acme_root: PathBuf, + maintenance_file: PathBuf, + forwarded_proto: String, + max_api_body_bytes: u64, + log_filter: String, + otel_enabled: bool, +} + +impl GatewayConfig { + fn from_env() -> io::Result { + Ok(Self { + listen_addr: read_env_or_default( + "GENARRATIVE_PINGORA_GATEWAY_LISTEN", + DEFAULT_LISTEN_ADDR, + ), + api_upstream: read_socket_addr_env( + "GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM", + DEFAULT_API_UPSTREAM, + )?, + spacetime_upstream: read_socket_addr_env( + "GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM", + DEFAULT_SPACETIME_UPSTREAM, + )?, + web_root: PathBuf::from(read_env_or_default( + "GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT", + DEFAULT_WEB_ROOT, + )), + acme_root: PathBuf::from(read_env_or_default( + "GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT", + DEFAULT_ACME_ROOT, + )), + maintenance_file: PathBuf::from(read_env_or_default( + "GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE", + DEFAULT_MAINTENANCE_FILE, + )), + forwarded_proto: read_env_or_default( + "GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO", + "http", + ), + max_api_body_bytes: read_u64_env( + "GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES", + DEFAULT_MAX_API_BODY_BYTES, + )?, + log_filter: read_env_or_default("GENARRATIVE_PINGORA_GATEWAY_LOG", DEFAULT_LOG_FILTER), + otel_enabled: read_bool_env("GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED", false), + }) + } +} + +#[derive(Clone)] +struct GenarrativeGateway { + config: Arc, +} + +#[derive(Debug)] +struct RequestContext { + request_id: String, + route: RouteDecision, + started_at: Instant, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ProxyTarget { + Api, + Spacetime, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum StaticRoot { + Web, + Acme, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum StaticMode { + Exact, + SpaFallback, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum LocalResponse { + RedirectPermanent { location: &'static str }, + Static { root: StaticRoot, mode: StaticMode }, + NotFound, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum RouteDecision { + Proxy { + target: ProxyTarget, + body_limit: Option, + }, + Local(LocalResponse), +} + +impl RouteDecision { + fn applies_maintenance_gate(&self) -> bool { + matches!( + self, + RouteDecision::Proxy { .. } + | RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + .. + }) + ) + } + + fn is_api_like(&self) -> bool { + matches!( + self, + RouteDecision::Proxy { + target: ProxyTarget::Api, + .. + } + ) + } + + fn proxy_target(&self) -> Option { + match self { + RouteDecision::Proxy { target, .. } => Some(*target), + RouteDecision::Local(_) => None, + } + } +} + +fn main() -> std::result::Result<(), Box> { + let config = Arc::new(GatewayConfig::from_env()?); + init_tracing( + &config.log_filter, + OtelConfig { + enabled: config.otel_enabled, + }, + )?; + + info!( + listen = %config.listen_addr, + api_upstream = %config.api_upstream, + spacetime_upstream = %config.spacetime_upstream, + web_root = %config.web_root.display(), + "Pingora shadow gateway 启动" + ); + + let opt = Opt::parse_args(); + let mut server = Server::new(Some(opt))?; + server.bootstrap(); + + let gateway = GenarrativeGateway { config }; + let listen_addr = gateway.config.listen_addr.clone(); + let mut proxy = http_proxy_service(&server.configuration, gateway); + proxy.add_tcp(&listen_addr); + server.add_service(proxy); + + server.run_forever(); +} + +#[async_trait] +impl ProxyHttp for GenarrativeGateway { + type CTX = RequestContext; + + fn new_ctx(&self) -> Self::CTX { + RequestContext { + request_id: Uuid::new_v4().to_string(), + route: RouteDecision::Local(LocalResponse::NotFound), + started_at: Instant::now(), + } + } + + async fn request_filter( + &self, + session: &mut Session, + ctx: &mut Self::CTX, + ) -> PingoraResult + where + Self::CTX: Send + Sync, + { + let path = session.req_header().uri.path(); + ctx.route = classify_path(path); + apply_configured_body_limit(&mut ctx.route, self.config.max_api_body_bytes); + ctx.request_id = resolve_request_id(session); + + if self.is_maintenance_enabled() && ctx.route.applies_maintenance_gate() { + respond_maintenance(session, ctx.route.is_api_like(), &self.config.web_root).await?; + return Ok(true); + } + + if let RouteDecision::Proxy { body_limit, .. } = ctx.route + && let Some(limit) = body_limit + && let Some(content_length) = content_length(session) + && content_length > limit + { + respond_json( + session, + 413, + r#"{"ok":false,"error":{"code":"PAYLOAD_TOO_LARGE","message":"请求体过大"}}"#, + ) + .await?; + return Ok(true); + } + + match &ctx.route { + RouteDecision::Proxy { .. } => Ok(false), + RouteDecision::Local(LocalResponse::RedirectPermanent { location }) => { + respond_redirect(session, location).await?; + Ok(true) + } + RouteDecision::Local(LocalResponse::Static { root, mode }) => { + let root_path = match root { + StaticRoot::Web => &self.config.web_root, + StaticRoot::Acme => &self.config.acme_root, + }; + serve_static(session, root_path, *mode).await?; + Ok(true) + } + RouteDecision::Local(LocalResponse::NotFound) => { + session.respond_error(404).await?; + Ok(true) + } + } + } + + async fn upstream_peer( + &self, + _session: &mut Session, + ctx: &mut Self::CTX, + ) -> PingoraResult> { + let upstream = match ctx.route.proxy_target().unwrap_or(ProxyTarget::Api) { + ProxyTarget::Api => self.config.api_upstream, + ProxyTarget::Spacetime => self.config.spacetime_upstream, + }; + + Ok(Box::new(HttpPeer::new(upstream, false, String::new()))) + } + + async fn upstream_request_filter( + &self, + session: &mut Session, + upstream_request: &mut pingora_http::RequestHeader, + ctx: &mut Self::CTX, + ) -> PingoraResult<()> + where + Self::CTX: Send + Sync, + { + upstream_request.insert_header("X-Request-Id", ctx.request_id.as_str())?; + upstream_request + .insert_header("X-Forwarded-Proto", self.config.forwarded_proto.as_str())?; + + if let Some(host) = session.req_header().headers.get("host").cloned() { + upstream_request.insert_header("Host", host.clone())?; + upstream_request.insert_header("X-Forwarded-Host", host)?; + } + + if let Some(client_ip) = client_ip(session) { + upstream_request.insert_header("X-Real-IP", client_ip.as_str())?; + let forwarded_for = append_forwarded_for(session, &client_ip); + upstream_request.insert_header("X-Forwarded-For", forwarded_for.as_str())?; + } + + // 中文注释:SpacetimeDB 订阅走 WebSocket Upgrade;普通 API 连接头保持干净,贴近当前 Nginx 模板。 + if ctx.route.proxy_target() == Some(ProxyTarget::Spacetime) && is_upgrade_request(session) { + upstream_request.insert_header("Connection", "Upgrade")?; + } else { + upstream_request.remove_header("connection"); + } + + Ok(()) + } + + async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + _ctx: &mut Self::CTX, + ) -> PingoraResult<()> + where + Self::CTX: Send + Sync, + { + upstream_response.insert_header("X-Genarrative-Gateway", "pingora-shadow")?; + Ok(()) + } + + async fn logging( + &self, + session: &mut Session, + error: Option<&pingora::Error>, + ctx: &mut Self::CTX, + ) where + Self::CTX: Send + Sync, + { + let status = session + .response_written() + .map_or(0, |response| response.status.as_u16()); + let elapsed_ms = ctx.started_at.elapsed().as_millis(); + let method = session.req_header().method.as_str(); + let path = session.req_header().uri.path(); + let route = format!("{:?}", ctx.route); + + if let Some(error) = error { + error!( + request_id = %ctx.request_id, + method, + path, + status, + route, + elapsed_ms, + %error, + "Pingora gateway request failed" + ); + } else { + info!( + request_id = %ctx.request_id, + method, + path, + status, + route, + elapsed_ms, + "Pingora gateway request completed" + ); + } + } +} + +impl GenarrativeGateway { + fn is_maintenance_enabled(&self) -> bool { + self.config.maintenance_file.exists() + } +} + +fn classify_path(path: &str) -> RouteDecision { + if path == "/admin" { + return RouteDecision::Local(LocalResponse::RedirectPermanent { + location: "/admin/", + }); + } + + if path.starts_with("/.well-known/acme-challenge/") { + return RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Acme, + mode: StaticMode::Exact, + }); + } + + if path.starts_with("/admin/api/") { + return RouteDecision::Proxy { + target: ProxyTarget::Api, + body_limit: None, + }; + } + + if is_gallery_list_path(path) { + return RouteDecision::Proxy { + target: ProxyTarget::Api, + body_limit: None, + }; + } + + if is_gallery_detail_path(path) { + return RouteDecision::Proxy { + target: ProxyTarget::Api, + body_limit: None, + }; + } + + if path == "/api" || path.starts_with("/api/") { + return RouteDecision::Proxy { + target: ProxyTarget::Api, + body_limit: Some(DEFAULT_MAX_API_BODY_BYTES), + }; + } + + // 中文注释:公网只转发前端 SDK 必需的 SpacetimeDB subscribe / identity 路由,其它 /v1 继续关闭。 + if is_spacetime_subscribe_path(path) || path.starts_with("/v1/identity") { + return RouteDecision::Proxy { + target: ProxyTarget::Spacetime, + body_limit: None, + }; + } + + if path.starts_with("/v1/") + || path.starts_with("/generated-") + || path.starts_with("/healthz") + || path.starts_with("/readyz") + { + return RouteDecision::Local(LocalResponse::NotFound); + } + + if path.starts_with("/admin/assets/") || path.starts_with("/assets/") { + return RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + mode: StaticMode::Exact, + }); + } + + if path.starts_with("/admin/") { + return RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + mode: StaticMode::SpaFallback, + }); + } + + RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + mode: StaticMode::SpaFallback, + }) +} + +fn apply_configured_body_limit(route: &mut RouteDecision, max_api_body_bytes: u64) { + if let RouteDecision::Proxy { + target: ProxyTarget::Api, + body_limit: Some(body_limit), + } = route + { + *body_limit = max_api_body_bytes; + } +} + +fn is_gallery_list_path(path: &str) -> bool { + matches!( + path, + "/api/runtime/puzzle/gallery" | "/api/runtime/custom-world-gallery" + ) +} + +fn is_gallery_detail_path(path: &str) -> bool { + let parts: Vec<_> = path.trim_matches('/').split('/').collect(); + matches!( + parts.as_slice(), + ["api", "runtime", "puzzle", "gallery", _] + | ["api", "runtime", "custom-world-gallery", _, _] + ) +} + +fn is_spacetime_subscribe_path(path: &str) -> bool { + let parts: Vec<_> = path.trim_matches('/').split('/').collect(); + matches!(parts.as_slice(), ["v1", "database", _, "subscribe"]) +} + +async fn serve_static(session: &mut Session, root: &Path, mode: StaticMode) -> PingoraResult<()> { + let path = session.req_header().uri.path(); + let Some(candidate) = resolve_static_candidate(root, path, mode).await else { + session.respond_error(404).await?; + return Ok(()); + }; + + let body = match tokio::fs::read(&candidate).await { + Ok(bytes) => Bytes::from(bytes), + Err(error) => { + warn!( + path = %path, + file = %candidate.display(), + %error, + "静态文件读取失败" + ); + session.respond_error(404).await?; + return Ok(()); + } + }; + let content_type = mime_guess::from_path(&candidate) + .first_or_octet_stream() + .essence_str() + .to_string(); + + respond_bytes(session, 200, Some(&content_type), body).await +} + +async fn resolve_static_candidate(root: &Path, path: &str, mode: StaticMode) -> Option { + let candidate = sanitize_static_path(root, path)?; + if is_regular_file(&candidate).await { + return Some(candidate); + } + + if is_directory(&candidate).await { + let index = candidate.join("index.html"); + if is_regular_file(&index).await { + return Some(index); + } + } + + match mode { + StaticMode::Exact => None, + StaticMode::SpaFallback if path.starts_with("/admin/") => { + let fallback = root.join("admin").join("index.html"); + is_regular_file(&fallback).await.then_some(fallback) + } + StaticMode::SpaFallback => { + let fallback = root.join("index.html"); + is_regular_file(&fallback).await.then_some(fallback) + } + } +} + +fn sanitize_static_path(root: &Path, path: &str) -> Option { + let mut result = root.to_path_buf(); + for component in Path::new(path.trim_start_matches('/')).components() { + match component { + Component::Normal(value) => result.push(value), + Component::CurDir => {} + Component::Prefix(_) | Component::RootDir | Component::ParentDir => return None, + } + } + Some(result) +} + +async fn is_regular_file(path: &Path) -> bool { + tokio::fs::metadata(path) + .await + .map(|metadata| metadata.is_file()) + .unwrap_or(false) +} + +async fn is_directory(path: &Path) -> bool { + tokio::fs::metadata(path) + .await + .map(|metadata| metadata.is_dir()) + .unwrap_or(false) +} + +async fn respond_maintenance( + session: &mut Session, + api_like: bool, + web_root: &Path, +) -> PingoraResult<()> { + if api_like { + return respond_json( + session, + 503, + r#"{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}"#, + ) + .await; + } + + let maintenance_page = web_root.join("maintenance.html"); + if is_regular_file(&maintenance_page).await { + let body = Bytes::from(tokio::fs::read(&maintenance_page).await.unwrap_or_default()); + respond_bytes(session, 503, Some("text/html; charset=utf-8"), body).await + } else { + respond_bytes( + session, + 503, + Some("text/plain; charset=utf-8"), + Bytes::from_static("服务维护中".as_bytes()), + ) + .await + } +} + +async fn respond_redirect(session: &mut Session, location: &str) -> PingoraResult<()> { + let mut header = ResponseHeader::build(301, Some(0))?; + header.insert_header("Location", location)?; + session.write_response_header(Box::new(header), true).await +} + +async fn respond_json(session: &mut Session, status: u16, body: &str) -> PingoraResult<()> { + respond_bytes( + session, + status, + Some("application/json; charset=utf-8"), + Bytes::copy_from_slice(body.as_bytes()), + ) + .await +} + +async fn respond_bytes( + session: &mut Session, + status: u16, + content_type: Option<&str>, + body: Bytes, +) -> PingoraResult<()> { + let is_head = session.req_header().method == Method::HEAD; + let mut header = ResponseHeader::build(status, Some(body.len()))?; + if let Some(content_type) = content_type { + header.insert_header("Content-Type", content_type)?; + } + header.insert_header("X-Genarrative-Gateway", "pingora-shadow")?; + session + .write_response_header(Box::new(header), is_head || body.is_empty()) + .await?; + if !is_head && !body.is_empty() { + session.write_response_body(Some(body), true).await?; + } + Ok(()) +} + +fn resolve_request_id(session: &Session) -> String { + session + .req_header() + .headers + .get("x-request-id") + .and_then(|value| value.to_str().ok()) + .filter(|value| !value.trim().is_empty()) + .map(ToOwned::to_owned) + .unwrap_or_else(|| Uuid::new_v4().to_string()) +} + +fn content_length(session: &Session) -> Option { + session + .req_header() + .headers + .get("content-length")? + .to_str() + .ok()? + .parse() + .ok() +} + +fn client_ip(session: &Session) -> Option { + session + .as_downstream() + .client_addr() + .map(|addr| addr.ip().to_string()) +} + +fn append_forwarded_for(session: &Session, client_ip: &str) -> String { + session + .req_header() + .headers + .get("x-forwarded-for") + .and_then(|value| value.to_str().ok()) + .filter(|value| !value.trim().is_empty()) + .map(|existing| format!("{existing}, {client_ip}")) + .unwrap_or_else(|| client_ip.to_string()) +} + +fn is_upgrade_request(session: &Session) -> bool { + session + .req_header() + .headers + .get("upgrade") + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.eq_ignore_ascii_case("websocket")) +} + +fn read_env_or_default(key: &str, default_value: &str) -> String { + env::var(key) + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| default_value.to_string()) +} + +fn read_bool_env(key: &str, default_value: bool) -> bool { + env::var(key) + .ok() + .and_then(|value| match value.trim().to_ascii_lowercase().as_str() { + "1" | "true" | "yes" | "on" => Some(true), + "0" | "false" | "no" | "off" => Some(false), + _ => None, + }) + .unwrap_or(default_value) +} + +fn read_u64_env(key: &str, default_value: u64) -> io::Result { + match env::var(key) { + Ok(value) if !value.trim().is_empty() => value.trim().parse().map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("{key} 不是有效整数:{error}"), + ) + }), + _ => Ok(default_value), + } +} + +fn read_socket_addr_env(key: &str, default_value: &str) -> io::Result { + let value = read_env_or_default(key, default_value); + value.parse().map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("{key} 不是有效 socket 地址 {value:?}:{error}"), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn classifies_nginx_api_routes_to_api_upstream() { + assert_eq!( + classify_path("/admin/api/users").proxy_target(), + Some(ProxyTarget::Api) + ); + assert_eq!( + classify_path("/api/runtime/puzzle/gallery").proxy_target(), + Some(ProxyTarget::Api) + ); + assert_eq!( + classify_path("/api/runtime/custom-world-gallery/profile-a/owner-b").proxy_target(), + Some(ProxyTarget::Api) + ); + assert_eq!( + classify_path("/api/creation-entry/config").proxy_target(), + Some(ProxyTarget::Api) + ); + } + + #[test] + fn applies_configured_body_limit_to_generic_api_routes() { + let mut route = classify_path("/api/creation-entry/config"); + apply_configured_body_limit(&mut route, 1024); + + assert_eq!( + route, + RouteDecision::Proxy { + target: ProxyTarget::Api, + body_limit: Some(1024), + } + ); + } + + #[test] + fn keeps_public_health_and_legacy_generated_paths_closed() { + assert_eq!( + classify_path("/healthz"), + RouteDecision::Local(LocalResponse::NotFound) + ); + assert_eq!( + classify_path("/readyz"), + RouteDecision::Local(LocalResponse::NotFound) + ); + assert_eq!( + classify_path("/healthz/internal"), + RouteDecision::Local(LocalResponse::NotFound) + ); + assert_eq!( + classify_path("/generated-puzzle-assets/foo.webp"), + RouteDecision::Local(LocalResponse::NotFound) + ); + } + + #[test] + fn only_forwards_minimal_spacetime_public_routes() { + assert_eq!( + classify_path("/v1/database/genarrative/subscribe").proxy_target(), + Some(ProxyTarget::Spacetime) + ); + assert_eq!( + classify_path("/v1/identity").proxy_target(), + Some(ProxyTarget::Spacetime) + ); + assert_eq!( + classify_path("/v1/ping"), + RouteDecision::Local(LocalResponse::NotFound) + ); + } + + #[test] + fn classifies_static_shell_routes() { + assert_eq!( + classify_path("/admin"), + RouteDecision::Local(LocalResponse::RedirectPermanent { + location: "/admin/" + }) + ); + assert_eq!( + classify_path("/admin/assets/index.js"), + RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + mode: StaticMode::Exact, + }) + ); + assert_eq!( + classify_path("/admin/settings"), + RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + mode: StaticMode::SpaFallback, + }) + ); + assert_eq!( + classify_path("/some/spa/path"), + RouteDecision::Local(LocalResponse::Static { + root: StaticRoot::Web, + mode: StaticMode::SpaFallback, + }) + ); + } + + #[test] + fn rejects_path_traversal_for_static_files() { + assert!(sanitize_static_path(Path::new("/srv/web"), "/assets/app.js").is_some()); + assert!(sanitize_static_path(Path::new("/srv/web"), "/assets/../secret").is_none()); + } +}