From df80876f6033919bbadb005f0f67a75b6449533a Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 7 May 2026 21:11:14 +0800 Subject: [PATCH] Consolidate workspace deps and migrate sha1 to sha2 --- .hermes/shared-memory/decision-log.md | 8 + .hermes/shared-memory/project-overview.md | 5 + ...XUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md | 8 +- docs/technical/README.md | 1 + ...ACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md | 55 +++ .../Jenkinsfile.production-stdb-module-build | 4 +- server-rs/.cargo/config.toml | 3 + server-rs/Cargo.lock | 65 ++-- server-rs/Cargo.toml | 56 ++- server-rs/README.md | 10 +- server-rs/crates/api-server/Cargo.toml | 91 +++-- server-rs/crates/api-server/src/assets.rs | 184 ++++++++- server-rs/crates/module-ai/Cargo.toml | 4 +- server-rs/crates/module-assets/Cargo.toml | 8 +- server-rs/crates/module-auth/Cargo.toml | 14 +- server-rs/crates/module-big-fish/Cargo.toml | 6 +- server-rs/crates/module-combat/Cargo.toml | 6 +- .../crates/module-custom-world/Cargo.toml | 4 +- server-rs/crates/module-inventory/Cargo.toml | 4 +- server-rs/crates/module-match3d/Cargo.toml | 4 +- server-rs/crates/module-npc/Cargo.toml | 4 +- .../crates/module-progression/Cargo.toml | 4 +- server-rs/crates/module-puzzle/Cargo.toml | 4 +- server-rs/crates/module-quest/Cargo.toml | 4 +- .../crates/module-runtime-item/Cargo.toml | 6 +- .../crates/module-runtime-story/Cargo.toml | 8 +- server-rs/crates/module-runtime/Cargo.toml | 8 +- .../crates/module-square-hole/Cargo.toml | 4 +- server-rs/crates/module-story/Cargo.toml | 14 +- server-rs/crates/platform-auth/Cargo.toml | 31 +- server-rs/crates/platform-auth/src/lib.rs | 163 ++++++-- server-rs/crates/platform-llm/Cargo.toml | 12 +- server-rs/crates/platform-oss/Cargo.toml | 17 +- server-rs/crates/platform-oss/src/lib.rs | 348 +++++++++++++++--- server-rs/crates/shared-contracts/Cargo.toml | 6 +- .../crates/shared-contracts/src/assets.rs | 30 +- server-rs/crates/shared-kernel/Cargo.toml | 4 +- server-rs/crates/shared-logging/Cargo.toml | 2 +- server-rs/crates/spacetime-client/Cargo.toml | 40 +- server-rs/crates/spacetime-module/Cargo.toml | 38 +- .../crates/spacetime-module/src/migration.rs | 4 +- 41 files changed, 949 insertions(+), 342 deletions(-) create mode 100644 docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 38a9590a..97c6d5f7 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-05-07 server-rs Cargo 依赖集中到 workspace + +- 背景:`server-rs` 多 crate 已稳定成 DDD workspace,成员 `Cargo.toml` 中重复散写第三方版本和本地 path 依赖,升级 SpacetimeDB SDK、`serde`、`reqwest`、`tokio` 等依赖时容易漂移。 +- 决策:`server-rs/Cargo.toml` 的 `[workspace.dependencies]` 统一维护第三方依赖版本和 workspace 内部 crate path;成员 crate 默认使用 `{ workspace = true }`,只保留自身 feature、optional 或 target-specific 差异;不再新增 `sha1`,OSS 与阿里云 OpenAPI 签名统一走 `sha2::Sha256` 对应的 V4/V3 口径。 +- 影响范围:`server-rs/Cargo.toml`、所有 `server-rs/crates/*/Cargo.toml`、`platform-oss`、`platform-auth`、后续新增 Rust crate 或新增 Rust 依赖的开发流程。 +- 验证方式:修改 Cargo 配置后先执行 `cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps`,再按影响范围执行 `cargo check`、DDD 边界检查和编码检查。 +- 关联文档:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md`。 + ## 2026-05-06 Maincloud 历史残留引用禁止再使用 - 背景:项目已经全面移除 Maincloud 运行口径,但历史脚本、测试名和文档仍可能让后续开发误用 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。 diff --git a/.hermes/shared-memory/project-overview.md b/.hermes/shared-memory/project-overview.md index 5918d97b..53b3aab7 100644 --- a/.hermes/shared-memory/project-overview.md +++ b/.hermes/shared-memory/project-overview.md @@ -81,6 +81,10 @@ DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md` 注意:`server-rs` 的默认 `cargo build` 只构建 `crates/api-server`,SpacetimeDB 模块产物继续走 `spacetime build` / 发布链路。 +Cargo 依赖口径:第三方依赖版本和 workspace 内部 crate path 统一维护在 `server-rs/Cargo.toml` 的 `[workspace.dependencies]`,成员 crate 默认继承 workspace 依赖,只保留自身 `features`、`optional` 或 target-specific 差异。 + +Rust 加密摘要依赖口径:新代码不再引入 `sha1`;OSS V4 签名、阿里云 OpenAPI V3 签名和 refresh session token 摘要统一使用 `sha2::Sha256`。 + ## SpacetimeDB 表域总览 以 `docs/technical/SPACETIMEDB_TABLE_CATALOG.md` 为持续维护入口。当前表域包括: @@ -135,4 +139,5 @@ DDD 分层边界以 `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md` - 契约与路由矩阵:`docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md` - SpacetimeDB 表结构变更约束:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` - SpacetimeDB 表目录:`docs/technical/SPACETIMEDB_TABLE_CATALOG.md` +- Rust workspace 依赖集中配置:`docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md` - 生产部署计划:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` diff --git a/docs/technical/PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md b/docs/technical/PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md index d5a6a6ff..be84d268 100644 --- a/docs/technical/PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md +++ b/docs/technical/PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md @@ -104,9 +104,11 @@ 1. 发送验证码调用 `SendSmsVerifyCode`。 2. 校验验证码调用 `CheckSmsVerifyCode`。 -3. 使用阿里云 RPC 签名口径: - - `SignatureMethod=HMAC-SHA1` - - `SignatureVersion=1.0` +3. 使用阿里云 OpenAPI V3 请求头签名口径: + - `Authorization: ACS3-HMAC-SHA256 ...` + - `x-acs-action` + - `x-acs-version` + - `x-acs-content-sha256` 4. 当前仍只支持中国大陆手机号。 ## 7. 状态与快照 diff --git a/docs/technical/README.md b/docs/technical/README.md index ad0f19f2..df7aaf03 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -4,6 +4,7 @@ ## 文档列表 +- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。 - [API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md](./API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md):冻结 api-server 外部服务配置边界,公共服务 URL 可保留代码默认值,非公共模型名和私有网关 URL 统一通过环境变量注入。 - [PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md](./PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md):冻结个人任务与埋点系统首版方案,明确 `tracking_event`、`tracking_daily_stat`、`profile_task_config`、任务进度、领奖记录和光点钱包流水的边界。 - [SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md](./SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md):记录方洞挑战结果页图片槽位局部生成、洞口图历史素材、运行态拖拽与点击投放交互的修正口径。 diff --git a/docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md b/docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md new file mode 100644 index 00000000..29fae1be --- /dev/null +++ b/docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md @@ -0,0 +1,55 @@ +# Rust workspace 依赖集中配置记录 + +日期:`2026-05-07` + +## 1. 背景 + +`server-rs` workspace 已经包含 `api-server`、`spacetime-module`、`spacetime-client`、多个 `module-*` 领域 crate、`platform-*` 适配 crate 和共享 crate。随着 DDD 收口推进,成员 `Cargo.toml` 中重复散写了第三方 crate 版本和本地 path 依赖,后续升级 `serde`、`reqwest`、`tokio`、`time`、SpacetimeDB SDK 或内部 crate 路径时容易出现漂移。 + +本次只做 Cargo 配置收敛,不改变业务代码、表结构、reducer/procedure 签名、HTTP contract 或前端绑定。 + +## 2. 配置规则 + +1. 共享第三方依赖版本统一维护在 `server-rs/Cargo.toml` 的 `[workspace.dependencies]`。 +2. workspace 内部 crate 的 `path` 也统一维护在根 `server-rs/Cargo.toml`。 +3. 成员 crate 默认使用 `{ workspace = true }` 继承依赖。 +4. 成员 crate 只保留自身需要表达的差异,例如 `features`、`optional = true` 或 target-specific dependency。 +5. 需要关闭 default features 的依赖,应优先在 workspace 根依赖中声明;成员 crate 不再重复覆盖同一项。 +6. `module-assets` 这类有默认服务端 feature 的领域 crate,在 workspace 根内按 `default-features = false` 维护;需要服务端 OSS/HTTP 能力的 adapter crate 显式启用 `features = ["server-service"]`。 + +## 3. 本次收敛范围 + +已上提到 workspace 根的依赖包括: + +1. 本地路径依赖:`module-*`、`platform-*`、`shared-*`、`spacetime-client`。 +2. 常用第三方依赖:`serde`、`serde_json`、`serde_urlencoded`、`reqwest`、`tokio`、`time`、`tracing`、`base64`、`hmac`、`sha2`、`uuid`、`url` 等。 +3. SpacetimeDB 相关依赖:`spacetimedb`、`spacetimedb-sdk`、`spacetimedb-lib`。 + +`spacetimedb-lib` 在 workspace 根统一关闭 default features,`spacetime-module` 只继承并补充 `features = ["serde"]`。这样避免成员 crate 尝试覆盖 workspace default-feature 设定导致 manifest 解析失败。 + +阿里云 OSS 相关签名不再依赖不推荐的 `sha1` crate,统一使用 `sha2::Sha256`: + +1. 浏览器直传 ticket 使用 OSS V4 表单签名字段:`x-oss-signature-version=OSS4-HMAC-SHA256`、`x-oss-credential`、`x-oss-date`、`x-oss-signature`。 +2. 服务端 OSS 读写请求和测试辅助签名统一使用 `OSS4-HMAC-SHA256` Authorization。 +3. 阿里云短信 OpenAPI 请求统一使用 `ACS3-HMAC-SHA256` 请求头签名,不再在表单中传旧 `SignatureMethod=HMAC-SHA1` / `SignatureVersion=1.0`。 + +## 4. 不在本次范围 + +1. 不新增或删除 crate。 +2. 不修改 `server-rs` workspace `members` / `default-members` 语义。 +3. 不修改 SpacetimeDB 表、reducer、procedure、migration 白名单或生成绑定。 +4. 不改变 `module-*` 的 DDD 依赖方向。 + +## 5. 验收口径 + +配置改动后至少执行: + +```powershell +cargo metadata --manifest-path server-rs\Cargo.toml --format-version 1 --no-deps +cargo check -p api-server --manifest-path server-rs\Cargo.toml +cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml +npm.cmd run check:server-rs-ddd +npm.cmd run check:encoding -- docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md docs/technical/README.md server-rs/README.md .hermes/shared-memory/decision-log.md .hermes/shared-memory/project-overview.md +``` + +若仅改 Cargo 依赖配置且未触碰 API smoke 相关代码,不强制启动 `npm run api-server`;若后续改动同时涉及 API 路由、SpacetimeDB facade 或运行时行为,仍按 `AGENTS.md` 和 DDD 文档执行后端 smoke。 diff --git a/jenkins/Jenkinsfile.production-stdb-module-build b/jenkins/Jenkinsfile.production-stdb-module-build index ffd7597c..c551e1a2 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-build +++ b/jenkins/Jenkinsfile.production-stdb-module-build @@ -1,6 +1,6 @@ pipeline { agent { - label 'built-in && windows' + label 'windows' } options { @@ -16,7 +16,7 @@ pipeline { CARGO_INCREMENTAL = '0' RUSTC_WRAPPER = 'sccache' SCCACHE_DIR = '${env.USERPROFILE}\\.cache\\sccache-stdb-module' - SCCACHE_CACHE_SIZE = '30G' + SCCACHE_CACHE_SIZE = '30G'o } parameters { diff --git a/server-rs/.cargo/config.toml b/server-rs/.cargo/config.toml index 525e9b4e..727ba8da 100644 --- a/server-rs/.cargo/config.toml +++ b/server-rs/.cargo/config.toml @@ -1,3 +1,6 @@ +[build] +rustc-wrapper = "sccache" + [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=lld"] diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index 8e1a4fba..1475ea09 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -75,7 +75,6 @@ dependencies = [ "dotenvy", "hmac", "http-body-util", - "httpdate", "image", "module-ai", "module-assets", @@ -98,7 +97,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha1", + "sha2", "shared-contracts", "shared-kernel", "shared-logging", @@ -1872,14 +1871,13 @@ name = "platform-auth" version = "0.1.0" dependencies = [ "argon2", - "base64 0.22.1", "hmac", "jsonwebtoken", "rand_core 0.6.4", "reqwest", "serde", "serde_json", - "sha1", + "serde_urlencoded", "sha2", "shared-kernel", "time", @@ -1906,11 +1904,10 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "hmac", - "httpdate", "reqwest", "serde", "serde_json", - "sha1", + "sha2", "time", "tokio", ] @@ -2730,9 +2727,9 @@ dependencies = [ [[package]] name = "spacetimedb" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591f9068644aab6808e7612a869dedde7eeb26df78027a19bc9dc597cc649678" +checksum = "1306cc3a9ed9c89f43b263614a529357cc53a067e3d06c1cbb485e3b577b118b" dependencies = [ "anyhow", "bytemuck", @@ -2753,9 +2750,9 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-macro" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f68bf4810d838be622c13efd4cd64e0a9ce8cd340deaa730f0c92caee845f9" +checksum = "51567ec01cd323438a00c134c16f26ffcde5f9dbe6a42a52e54578285bf49d73" dependencies = [ "heck 0.4.1", "humantime", @@ -2767,18 +2764,18 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-sys" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c2fe9f4124a599c9deae8f8231be3ae5a49bc5b2eef5e04c04b2632cf4cc0b4" +checksum = "3b40fa1bea26664085febe2b4455568c8b47dea2cb0245406b27e30963df2ba1" dependencies = [ "spacetimedb-primitives", ] [[package]] name = "spacetimedb-client-api-messages" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a18c2e145f61ad498f8094a2231e09f8d39a3dde09defa716075dbcb8c7e85" +checksum = "dfc9eeb20a555bad07029cbee4efe3a305cb5c1e40e21a07cbbbbed16a106014" dependencies = [ "bytes", "bytestring", @@ -2798,9 +2795,9 @@ dependencies = [ [[package]] name = "spacetimedb-data-structures" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4035c17ddbfc8c49a659bd6fb265b0a2a11115d1b4ad1963bccfad75cdfb4b" +checksum = "748fd5850a757823c5b8948065d9e4dc5092968a051aa3f34f170e91d95e493b" dependencies = [ "ahash", "crossbeam-queue", @@ -2813,9 +2810,9 @@ dependencies = [ [[package]] name = "spacetimedb-lib" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "672c0dd16feced67155a0dee7bd38d30f7725321c8177cb871a21c3d8749ae97" +checksum = "5612611d09d358f535438275d2a0d6a5e2fa56fa583dcfdbeddd623974df1d5e" dependencies = [ "anyhow", "bitflags", @@ -2837,9 +2834,9 @@ dependencies = [ [[package]] name = "spacetimedb-memory-usage" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c00614eb981354ee6b31661ec47002d3fc274f9d4543279dd6ee8692cdd8266" +checksum = "1c3a0d08fc5d8688a47e3ffcb803275519663b7ea1fba7ad25e608182de4ec6d" dependencies = [ "decorum", "ethnum", @@ -2847,9 +2844,9 @@ dependencies = [ [[package]] name = "spacetimedb-metrics" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6f7f6b24932505a696b75b7e5e60646ab1d76eeb8d2f95f04948562c965b5e" +checksum = "ca2d647201339aa17ba438a07463e96ed64ba214fb0c182588e262b055efa7f3" dependencies = [ "arrayvec", "itertools", @@ -2859,9 +2856,9 @@ dependencies = [ [[package]] name = "spacetimedb-primitives" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba5d7497d54aa8d4254f78a0bef12606bb05e62f8dea8b69abc9b241508e8b7" +checksum = "9b668b51e7318207ae7eebcd4cae0c5d43bf713e7f229ac309ea2614a486ffde" dependencies = [ "bitflags", "either", @@ -2873,18 +2870,18 @@ dependencies = [ [[package]] name = "spacetimedb-query-builder" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04c6e41e05273f14405ac6f429477626677d46528b561a509b7b78b45128f30" +checksum = "0186b1a2b3bf25bdd0f2676b61801fd754013ca6a58e1e24cc5148945388bc9d" dependencies = [ "spacetimedb-lib", ] [[package]] name = "spacetimedb-sats" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfde33ec86d80881da8b00c42096bf0382bef8e1bc35e9b6faaa42d77cbf503c" +checksum = "11780ed69f178bf3784b7599da5171450e4b7ac6fd66b79e2e1861c867cef1a6" dependencies = [ "anyhow", "arrayvec", @@ -2915,9 +2912,9 @@ dependencies = [ [[package]] name = "spacetimedb-schema" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b03b34a38bd39f3f60a0687efafb942355bd9f6026b88a38c7c9ec904e944f1" +checksum = "1e4e9f8aa596e0e7034f0c8b3649d3fa3cc7bde340761519c3a3c60f10ec8888" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -2946,9 +2943,9 @@ dependencies = [ [[package]] name = "spacetimedb-sdk" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7302851fec72929ffef976125f51971b0ec76be2730e27705a8e544f2ce159" +checksum = "41e82f20034b8aaeaa081871b07895aab45be1f0fc35e114ab64ae8e7e5c1a54" dependencies = [ "anymap3", "base64 0.21.7", @@ -2978,9 +2975,9 @@ dependencies = [ [[package]] name = "spacetimedb-sql-parser" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cbb9a837ac5f1ddb0cfb745159dea276dcf456244452f5a90684e5184f1f31" +checksum = "ec5c77a2d4e3f42ede59598c56cb81a0fe54fd1974e2707f7140d1d5f41d08a7" dependencies = [ "derive_more", "spacetimedb-lib", diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index ae27db5d..74af3855 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -42,8 +42,62 @@ version = "0.1.0" license = "UNLICENSED" [workspace.dependencies] +# 本地 workspace crate 路径统一在这里维护,成员 crate 只声明 feature 差异。 +module-ai = { path = "crates/module-ai", default-features = false } +module-assets = { path = "crates/module-assets", default-features = false } +module-auth = { path = "crates/module-auth", default-features = false } +module-big-fish = { path = "crates/module-big-fish", default-features = false } +module-combat = { path = "crates/module-combat", default-features = false } +module-custom-world = { path = "crates/module-custom-world", default-features = false } +module-inventory = { path = "crates/module-inventory", default-features = false } +module-match3d = { path = "crates/module-match3d", default-features = false } +module-npc = { path = "crates/module-npc", default-features = false } +module-progression = { path = "crates/module-progression", default-features = false } +module-puzzle = { path = "crates/module-puzzle", default-features = false } +module-quest = { path = "crates/module-quest", default-features = false } +module-runtime = { path = "crates/module-runtime", default-features = false } +module-runtime-item = { path = "crates/module-runtime-item", default-features = false } +module-runtime-story = { path = "crates/module-runtime-story", default-features = false } +module-square-hole = { path = "crates/module-square-hole", default-features = false } +module-story = { path = "crates/module-story", default-features = false } +platform-auth = { path = "crates/platform-auth", default-features = false } +platform-llm = { path = "crates/platform-llm", default-features = false } +platform-oss = { path = "crates/platform-oss", 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 } +spacetime-client = { path = "crates/spacetime-client", default-features = false } + +argon2 = "0.5" +async-stream = "0.3" +axum = "0.8" +base64 = "0.22" +dotenvy = "0.15" +hmac = "0.12" +http-body-util = "0.1" +image = { version = "0.25", default-features = false } +jsonwebtoken = "9" log = "0.4" -spacetimedb = "2.1.0" +rand_core = "0.6" +reqwest = { version = "0.12", default-features = false } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_urlencoded = "0.7" +sha2 = "0.10" +spacetimedb = "2.2.0" +spacetimedb-sdk = "2.2.0" +spacetimedb-lib = { version = "2.2.0", default-features = false } +time = "0.3" +tokio = "1" +tokio-stream = "0.1" +tower = "0.5" +tower-http = "0.6" +tracing = "0.1" +tracing-subscriber = "0.3" +url = "2" +urlencoding = "2" +uuid = "1" +webp = "0.3" [profile.dev] opt-level = 0 # 默认 0,有人手滑改 1/2 会慢 diff --git a/server-rs/README.md b/server-rs/README.md index 947cca84..43143475 100644 --- a/server-rs/README.md +++ b/server-rs/README.md @@ -14,7 +14,7 @@ ## 2. 当前阶段说明 -当前目录已经完成以下三十七项初始化: +当前目录已经完成以下三十八项初始化: 1. 为新后端预留正式目录并把路径固定到仓库结构中。 2. 创建虚拟 workspace `Cargo.toml`,后续 crate 会逐项挂入。 @@ -53,6 +53,7 @@ 35. 创建 `scripts/spacetime-dev.sh`,固定 Unix-like 本地 SpacetimeDB 启动入口。 36. 创建 `scripts/oss-smoke.ps1`,固定 Windows 本地阿里云 OSS 真实联调入口。 37. 固定 Vite dev proxy 的 Rust `api-server` 默认目标与 `GENARRATIVE_RUNTIME_SERVER_TARGET` 覆盖开关。 +38. 固定 `Cargo.toml` 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`。 后续任务会继续在本目录内按顺序补齐: @@ -109,8 +110,15 @@ 4. `spacetime-module` 新增业务入口前先确认是否已有对应上下文目录,禁止继续把大段业务流程堆回 `src/lib.rs`。 5. 根目录可执行 `npm run check:server-rs-ddd` 检查第一阶段 DDD 骨架与绝对边界。 +## 7. Cargo 依赖配置口径 + +`2026-05-07` 起,`server-rs` 的依赖版本和 workspace 内部 crate path 统一维护在根 `Cargo.toml` 的 `[workspace.dependencies]`,完整记录见 [../docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](../docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md)。 + +成员 crate 的 `Cargo.toml` 默认使用 `{ workspace = true }` 继承依赖;只在成员 crate 内保留本 crate 的 feature、optional、target-specific dependency 等差异。新增 crate 或新增依赖时,应优先补根 workspace 依赖,再在成员 crate 中继承。 + ## 5. 关联文档 1. [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) 2. [../backend-rewrite-tasklist/M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](../backend-rewrite-tasklist/M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md) 3. [../backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md](../backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md) +4. [../docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](../docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md) diff --git a/server-rs/crates/api-server/Cargo.toml b/server-rs/crates/api-server/Cargo.toml index dcb23ed2..6d190c19 100644 --- a/server-rs/crates/api-server/Cargo.toml +++ b/server-rs/crates/api-server/Cargo.toml @@ -5,51 +5,50 @@ version.workspace = true license.workspace = true [dependencies] -async-stream = "0.3" -axum = "0.8" -base64 = "0.22" -dotenvy = "0.15" -image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -webp = "0.3" -module-ai = { path = "../module-ai" } -module-assets = { path = "../module-assets" } -module-auth = { path = "../module-auth" } -module-big-fish = { path = "../module-big-fish" } -module-combat = { path = "../module-combat" } -module-custom-world = { path = "../module-custom-world" } -module-inventory = { path = "../module-inventory" } -module-match3d = { path = "../module-match3d" } -module-npc = { path = "../module-npc" } -module-puzzle = { path = "../module-puzzle" } -module-runtime = { path = "../module-runtime" } -module-runtime-story = { path = "../module-runtime-story" } -module-runtime-item = { path = "../module-runtime-item" } -module-square-hole = { path = "../module-square-hole" } -module-story = { path = "../module-story" } -platform-auth = { path = "../platform-auth" } -platform-llm = { path = "../platform-llm" } -platform-oss = { path = "../platform-oss" } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -shared-contracts = { path = "../shared-contracts" } -shared-kernel = { path = "../shared-kernel" } -shared-logging = { path = "../shared-logging" } -spacetime-client = { path = "../spacetime-client" } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "time"] } -tokio-stream = "0.1" -time = { version = "0.3", features = ["formatting"] } -tower-http = { version = "0.6", features = ["trace"] } -tracing = "0.1" -url = "2" -urlencoding = "2" -uuid = { version = "1", features = ["v4"] } +async-stream = { workspace = true } +axum = { workspace = true } +base64 = { workspace = true } +dotenvy = { workspace = true } +image = { workspace = true, features = ["jpeg", "png", "webp"] } +reqwest = { workspace = true, features = ["json", "rustls-tls"] } +webp = { workspace = true } +module-ai = { workspace = true } +module-assets = { workspace = true, features = ["server-service"] } +module-auth = { workspace = true } +module-big-fish = { workspace = true } +module-combat = { workspace = true } +module-custom-world = { workspace = true } +module-inventory = { workspace = true } +module-match3d = { workspace = true } +module-npc = { workspace = true } +module-puzzle = { workspace = true } +module-runtime = { workspace = true } +module-runtime-story = { workspace = true } +module-runtime-item = { workspace = true } +module-square-hole = { workspace = true } +module-story = { workspace = true } +platform-auth = { workspace = true } +platform-llm = { workspace = true } +platform-oss = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +shared-contracts = { workspace = true } +shared-kernel = { workspace = true } +shared-logging = { workspace = true } +spacetime-client = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time"] } +tokio-stream = { workspace = true } +time = { workspace = true, features = ["formatting"] } +tower-http = { workspace = true, features = ["trace"] } +tracing = { workspace = true } +url = { workspace = true } +urlencoding = { workspace = true } +uuid = { workspace = true, features = ["v4"] } [dev-dependencies] -base64 = "0.22" -hmac = "0.12" -httpdate = "1" -http-body-util = "0.1" -reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls"] } -sha1 = "0.10" -tower = { version = "0.5", features = ["util"] } +base64 = { workspace = true } +hmac = { workspace = true } +http-body-util = { workspace = true } +reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] } +sha2 = { workspace = true } +tower = { workspace = true, features = ["util"] } diff --git a/server-rs/crates/api-server/src/assets.rs b/server-rs/crates/api-server/src/assets.rs index d0605df2..a830b3b6 100644 --- a/server-rs/crates/api-server/src/assets.rs +++ b/server-rs/crates/api-server/src/assets.rs @@ -473,26 +473,23 @@ mod tests { error::Error, fs, path::{Path, PathBuf}, - time::SystemTime, }; use axum::{ body::Body, http::{Request, StatusCode}, }; - use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use hmac::{Hmac, Mac}; use http_body_util::BodyExt; - use httpdate::fmt_http_date; use reqwest::{Method, multipart}; use serde_json::{Value, json}; - use sha1::Sha1; + use sha2::{Digest, Sha256}; use shared_kernel::new_uuid_simple_string; use tower::ServiceExt; use crate::{app::build_router, config::AppConfig, state::AppState}; - type HmacSha1 = Hmac; + type HmacSha256 = Hmac; #[test] fn asset_history_kind_support_includes_puzzle_cover_image() { @@ -653,8 +650,13 @@ mod tests { Value::String("private".to_string()) ); assert_eq!( - payload["data"]["upload"]["formFields"]["OSSAccessKeyId"], - Value::String("test-access-key-id".to_string()) + payload["data"]["upload"]["formFields"]["x-oss-signature-version"], + Value::String("OSS4-HMAC-SHA256".to_string()) + ); + assert!( + payload["data"]["upload"]["formFields"]["x-oss-credential"] + .as_str() + .is_some_and(|value| value.starts_with("test-access-key-id/")) ); assert!(payload["data"]["upload"].get("publicUrl").is_none()); } @@ -702,7 +704,7 @@ mod tests { assert!( payload["data"]["read"]["signedUrl"] .as_str() - .is_some_and(|value| value.contains("OSSAccessKeyId=test-access-key-id")) + .is_some_and(|value| value.contains("x-oss-signature-version=OSS4-HMAC-SHA256")) ); } @@ -1410,13 +1412,26 @@ mod tests { .oss_access_key_secret .as_deref() .ok_or_else(|| std::io::Error::other("缺少 oss access key secret"))?; - let date = fmt_http_date(SystemTime::now()); - let canonical_resource = match object_key.map(str::trim).filter(|value| !value.is_empty()) { - Some(object_key) => format!("/{bucket}/{}", object_key.trim_start_matches('/')), - None => format!("/{bucket}/"), - }; - let string_to_sign = format!("{}\n\n\n{}\n{}", method.as_str(), date, canonical_resource); - let signature = sign_oss_string(access_key_secret, &string_to_sign)?; + let signed_at = time::OffsetDateTime::now_utc(); + let signed_at_text = build_oss_v4_signature_date(signed_at); + let signature_scope = build_oss_v4_signature_scope(endpoint, signed_at)?; + let object_path = object_key.map(str::trim).filter(|value| !value.is_empty()); + let canonical_uri = build_oss_v4_canonical_uri(bucket, object_path); + let payload_hash = "UNSIGNED-PAYLOAD"; + let canonical_headers = + format!("host:{bucket}.{endpoint}\nx-oss-content-sha256:{payload_hash}\nx-oss-date:{signed_at_text}\n"); + let additional_headers = "host"; + let canonical_request = format!( + "{}\n{}\n\n{}\n{}\n{}", + method.as_str(), + canonical_uri, + canonical_headers, + additional_headers, + payload_hash + ); + let string_to_sign = + build_oss_v4_string_to_sign(&signed_at_text, &signature_scope, &canonical_request); + let signature = sign_oss_v4_content(access_key_secret, &signature_scope, &string_to_sign)?; let target_url = match object_key.map(str::trim).filter(|value| !value.is_empty()) { Some(object_key) => build_object_url(config, object_key)?, None => reqwest::Url::parse(&format!("https://{bucket}.{endpoint}/"))?, @@ -1424,18 +1439,147 @@ mod tests { let response = client .request(method, target_url) - .header("Date", date) - .header("Authorization", format!("OSS {access_key_id}:{signature}")) + .header("x-oss-content-sha256", payload_hash) + .header("x-oss-date", signed_at_text) + .header( + "Authorization", + format!( + "OSS4-HMAC-SHA256 Credential={access_key_id}/{signature_scope},AdditionalHeaders={additional_headers},Signature={signature}" + ), + ) .send() .await?; Ok(response) } - fn sign_oss_string(secret: &str, content: &str) -> Result> { - let mut signer = HmacSha1::new_from_slice(secret.as_bytes())?; + fn build_oss_v4_signature_scope( + endpoint: &str, + signed_at: time::OffsetDateTime, + ) -> Result> { + let date = signed_at.date().to_string().replace('-', ""); + let region = endpoint + .trim() + .split('.') + .next() + .and_then(|segment| segment.strip_prefix("oss-")) + .ok_or_else(|| std::io::Error::other("OSS endpoint 无法解析 region"))?; + + Ok(format!("{date}/{region}/oss/aliyun_v4_request")) + } + + fn build_oss_v4_signature_date(signed_at: time::OffsetDateTime) -> String { + let date = signed_at.date().to_string().replace('-', ""); + let time = signed_at + .time() + .to_string() + .split('.') + .next() + .unwrap_or("00:00:00") + .replace(':', ""); + + debug_assert_eq!(time.len(), 6); + format!("{date}T{time}Z") + } + + fn build_oss_v4_canonical_uri(bucket: &str, object_key: Option<&str>) -> String { + match object_key.map(str::trim).filter(|value| !value.is_empty()) { + Some(object_key) => format!( + "/{}/{}", + encode_oss_url_query_value(bucket), + encode_oss_url_path(object_key.trim_start_matches('/')) + ), + None => format!("/{}/", encode_oss_url_query_value(bucket)), + } + } + + fn build_oss_v4_string_to_sign( + signature_date: &str, + signature_scope: &str, + canonical_request: &str, + ) -> String { + format!( + "OSS4-HMAC-SHA256\n{signature_date}\n{signature_scope}\n{}", + sha256_hex(canonical_request.as_bytes()) + ) + } + + fn sign_oss_v4_content( + secret: &str, + signature_scope: &str, + content: &str, + ) -> Result> { + let signing_key = build_oss_v4_signing_key(secret, signature_scope)?; + let mut signer = HmacSha256::new_from_slice(&signing_key)?; signer.update(content.as_bytes()); - Ok(BASE64_STANDARD.encode(signer.finalize().into_bytes())) + Ok(hex_lower(&signer.finalize().into_bytes())) + } + + fn build_oss_v4_signing_key( + secret: &str, + signature_scope: &str, + ) -> Result, Box> { + let mut parts = signature_scope.split('/'); + let date = parts + .next() + .ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少日期"))?; + let region = parts + .next() + .ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少 region"))?; + let service = parts + .next() + .ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少 service"))?; + let request = parts + .next() + .ok_or_else(|| std::io::Error::other("OSS V4 scope 缺少 request"))?; + + let date_key = hmac_sha256_raw(format!("aliyun_v4{secret}").as_bytes(), date)?; + let region_key = hmac_sha256_raw(&date_key, region)?; + let service_key = hmac_sha256_raw(®ion_key, service)?; + hmac_sha256_raw(&service_key, request) + } + + fn hmac_sha256_raw(key: &[u8], content: &str) -> Result, Box> { + let mut signer = HmacSha256::new_from_slice(key)?; + signer.update(content.as_bytes()); + Ok(signer.finalize().into_bytes().to_vec()) + } + + fn sha256_hex(content: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(content); + hex_lower(&hasher.finalize()) + } + + fn hex_lower(bytes: &[u8]) -> String { + bytes + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + } + + fn encode_oss_url_path(path: &str) -> String { + path.split('/') + .map(encode_oss_url_query_value) + .collect::>() + .join("/") + } + + fn encode_oss_url_query_value(value: &str) -> String { + let mut encoded = String::with_capacity(value.len()); + for byte in value.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + encoded.push(byte as char) + } + _ => { + use std::fmt::Write as _; + + let _ = write!(&mut encoded, "%{byte:02X}"); + } + } + } + encoded } fn ensure_success_status(status: u16, message: &str) -> Result<(), Box> { diff --git a/server-rs/crates/module-ai/Cargo.toml b/server-rs/crates/module-ai/Cargo.toml index fb8516e5..990e938f 100644 --- a/server-rs/crates/module-ai/Cargo.toml +++ b/server-rs/crates/module-ai/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-assets/Cargo.toml b/server-rs/crates/module-assets/Cargo.toml index f7293d8c..8522ac37 100644 --- a/server-rs/crates/module-assets/Cargo.toml +++ b/server-rs/crates/module-assets/Cargo.toml @@ -10,8 +10,8 @@ server-service = ["dep:platform-oss", "dep:reqwest"] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"], optional = true } +serde = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"], optional = true } spacetimedb = { workspace = true, optional = true } -platform-oss = { path = "../platform-oss", optional = true } -shared-kernel = { path = "../shared-kernel" } +platform-oss = { workspace = true, optional = true } +shared-kernel = { workspace = true } diff --git a/server-rs/crates/module-auth/Cargo.toml b/server-rs/crates/module-auth/Cargo.toml index b5749c82..eb7fa7b5 100644 --- a/server-rs/crates/module-auth/Cargo.toml +++ b/server-rs/crates/module-auth/Cargo.toml @@ -5,12 +5,12 @@ version.workspace = true license.workspace = true [dependencies] -platform-auth = { path = "../platform-auth" } -shared-kernel = { path = "../shared-kernel" } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -time = { version = "0.3", features = ["formatting", "parsing"] } -tracing = "0.1" +platform-auth = { workspace = true } +shared-kernel = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +time = { workspace = true, features = ["formatting", "parsing"] } +tracing = { workspace = true } [dev-dependencies] -tokio = { version = "1", features = ["macros", "rt"] } +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/server-rs/crates/module-big-fish/Cargo.toml b/server-rs/crates/module-big-fish/Cargo.toml index f79978b1..e940f089 100644 --- a/server-rs/crates/module-big-fish/Cargo.toml +++ b/server-rs/crates/module-big-fish/Cargo.toml @@ -9,7 +9,7 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +serde_json = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-combat/Cargo.toml b/server-rs/crates/module-combat/Cargo.toml index 30aa15b1..0600ba80 100644 --- a/server-rs/crates/module-combat/Cargo.toml +++ b/server-rs/crates/module-combat/Cargo.toml @@ -9,7 +9,7 @@ default = [] spacetime-types = ["dep:spacetimedb", "module-runtime-item/spacetime-types"] [dependencies] -module-runtime-item = { path = "../module-runtime-item", default-features = false } -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +module-runtime-item = { workspace = true } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-custom-world/Cargo.toml b/server-rs/crates/module-custom-world/Cargo.toml index c14bdba6..fc587abe 100644 --- a/server-rs/crates/module-custom-world/Cargo.toml +++ b/server-rs/crates/module-custom-world/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde = { workspace = true } +serde_json = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-inventory/Cargo.toml b/server-rs/crates/module-inventory/Cargo.toml index b12531ba..4f41e104 100644 --- a/server-rs/crates/module-inventory/Cargo.toml +++ b/server-rs/crates/module-inventory/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-match3d/Cargo.toml b/server-rs/crates/module-match3d/Cargo.toml index 5e5042f3..2fd47fe7 100644 --- a/server-rs/crates/module-match3d/Cargo.toml +++ b/server-rs/crates/module-match3d/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-npc/Cargo.toml b/server-rs/crates/module-npc/Cargo.toml index 957bb219..17a32485 100644 --- a/server-rs/crates/module-npc/Cargo.toml +++ b/server-rs/crates/module-npc/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-progression/Cargo.toml b/server-rs/crates/module-progression/Cargo.toml index 9b87b8d6..3886bf46 100644 --- a/server-rs/crates/module-progression/Cargo.toml +++ b/server-rs/crates/module-progression/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-puzzle/Cargo.toml b/server-rs/crates/module-puzzle/Cargo.toml index abcd920f..90be0dc8 100644 --- a/server-rs/crates/module-puzzle/Cargo.toml +++ b/server-rs/crates/module-puzzle/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-quest/Cargo.toml b/server-rs/crates/module-quest/Cargo.toml index 25d81420..411ab570 100644 --- a/server-rs/crates/module-quest/Cargo.toml +++ b/server-rs/crates/module-quest/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-runtime-item/Cargo.toml b/server-rs/crates/module-runtime-item/Cargo.toml index 7378e261..fcb136ed 100644 --- a/server-rs/crates/module-runtime-item/Cargo.toml +++ b/server-rs/crates/module-runtime-item/Cargo.toml @@ -9,7 +9,7 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -module-inventory = { path = "../module-inventory", default-features = false } -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +module-inventory = { workspace = true } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-runtime-story/Cargo.toml b/server-rs/crates/module-runtime-story/Cargo.toml index 8242922f..aa0253a6 100644 --- a/server-rs/crates/module-runtime-story/Cargo.toml +++ b/server-rs/crates/module-runtime-story/Cargo.toml @@ -5,7 +5,7 @@ version.workspace = true license.workspace = true [dependencies] -serde_json = "1" -shared-contracts = { path = "../shared-contracts" } -shared-kernel = { path = "../shared-kernel" } -time = { version = "0.3", features = ["formatting"] } +serde_json = { workspace = true } +shared-contracts = { workspace = true } +shared-kernel = { workspace = true } +time = { workspace = true, features = ["formatting"] } diff --git a/server-rs/crates/module-runtime/Cargo.toml b/server-rs/crates/module-runtime/Cargo.toml index 8e69e4ca..786ff327 100644 --- a/server-rs/crates/module-runtime/Cargo.toml +++ b/server-rs/crates/module-runtime/Cargo.toml @@ -9,8 +9,8 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +serde_json = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } -time = { version = "0.3", features = ["formatting", "parsing"] } +time = { workspace = true, features = ["formatting", "parsing"] } diff --git a/server-rs/crates/module-square-hole/Cargo.toml b/server-rs/crates/module-square-hole/Cargo.toml index 4d9c8e23..68ee1304 100644 --- a/server-rs/crates/module-square-hole/Cargo.toml +++ b/server-rs/crates/module-square-hole/Cargo.toml @@ -9,6 +9,6 @@ default = [] spacetime-types = ["dep:spacetimedb"] [dependencies] -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-story/Cargo.toml b/server-rs/crates/module-story/Cargo.toml index e34b603a..647a8ae7 100644 --- a/server-rs/crates/module-story/Cargo.toml +++ b/server-rs/crates/module-story/Cargo.toml @@ -16,11 +16,11 @@ spacetime-types = [ ] [dependencies] -module-combat = { path = "../module-combat", default-features = false } -module-inventory = { path = "../module-inventory", default-features = false } -module-progression = { path = "../module-progression", default-features = false } -module-quest = { path = "../module-quest", default-features = false } -module-runtime-item = { path = "../module-runtime-item", default-features = false } -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } +module-combat = { workspace = true } +module-inventory = { workspace = true } +module-progression = { workspace = true } +module-quest = { workspace = true } +module-runtime-item = { workspace = true } +serde = { workspace = true } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/platform-auth/Cargo.toml b/server-rs/crates/platform-auth/Cargo.toml index 1214d342..67cda152 100644 --- a/server-rs/crates/platform-auth/Cargo.toml +++ b/server-rs/crates/platform-auth/Cargo.toml @@ -5,21 +5,20 @@ version.workspace = true license.workspace = true [dependencies] -argon2 = "0.5" -base64 = "0.22" -hmac = "0.12" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -serde_json = "1" -sha1 = "0.10" -sha2 = "0.10" -jsonwebtoken = "9" -rand_core = { version = "0.6", features = ["getrandom"] } -serde = { version = "1", features = ["derive"] } -shared-kernel = { path = "../shared-kernel" } -time = { version = "0.3", features = ["std"] } -tracing = "0.1" -url = "2" -urlencoding = "2" +argon2 = { workspace = true } +hmac = { workspace = true } +reqwest = { workspace = true, features = ["json", "rustls-tls"] } +serde_json = { workspace = true } +serde_urlencoded = { workspace = true } +sha2 = { workspace = true } +jsonwebtoken = { workspace = true } +rand_core = { workspace = true, features = ["getrandom"] } +serde = { workspace = true } +shared-kernel = { workspace = true } +time = { workspace = true, features = ["std"] } +tracing = { workspace = true } +url = { workspace = true } +urlencoding = { workspace = true } [dev-dependencies] -tokio = { version = "1", features = ["macros", "rt"] } +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/server-rs/crates/platform-auth/src/lib.rs b/server-rs/crates/platform-auth/src/lib.rs index 6f05894d..b64a660a 100644 --- a/server-rs/crates/platform-auth/src/lib.rs +++ b/server-rs/crates/platform-auth/src/lib.rs @@ -5,7 +5,6 @@ use std::{ }; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString}; -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use hmac::{Hmac, Mac}; use jsonwebtoken::{ Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind, @@ -14,7 +13,6 @@ use rand_core::OsRng; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sha1::Sha1; use sha2::{Digest, Sha256}; use shared_kernel::{new_uuid_simple_string, normalize_optional_string, normalize_required_string}; use time::{Duration, OffsetDateTime}; @@ -43,7 +41,7 @@ pub const DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT: &str = "https://api.weixin.qq.com/sns/oauth2/access_token"; pub const DEFAULT_WECHAT_USER_INFO_ENDPOINT: &str = "https://api.weixin.qq.com/sns/userinfo"; -type HmacSha1 = Hmac; +type HmacSha256 = Hmac; // 鉴权 provider 直接冻结成文档中约定的枚举,避免后续在多个 crate 内重复发明字符串字面量。 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -927,14 +925,6 @@ impl AliyunSmsAuthProvider { query.insert("Action".to_string(), "SendSmsVerifyCode".to_string()); query.insert("Format".to_string(), "json".to_string()); query.insert("Version".to_string(), "2017-05-25".to_string()); - query.insert("Timestamp".to_string(), current_aliyun_timestamp()); - query.insert("SignatureNonce".to_string(), new_uuid_simple_string()); - query.insert("SignatureMethod".to_string(), "HMAC-SHA1".to_string()); - query.insert("SignatureVersion".to_string(), "1.0".to_string()); - query.insert( - "AccessKeyId".to_string(), - self.config.access_key_id.clone().unwrap_or_default(), - ); query.insert( "PhoneNumber".to_string(), request.national_phone_number.trim().to_string(), @@ -971,11 +961,12 @@ impl AliyunSmsAuthProvider { if let Some(scheme_name) = self.config.scheme_name.clone() { query.insert("SchemeName".to_string(), scheme_name); } - self.sign_query(&mut query)?; + let signature_headers = self.build_signature_headers("SendSmsVerifyCode", &query)?; let payload = self .client .post(build_aliyun_sms_url(&self.config.endpoint)?) + .headers(signature_headers) .form(&query) .send() .await @@ -1053,14 +1044,6 @@ impl AliyunSmsAuthProvider { query.insert("Action".to_string(), "CheckSmsVerifyCode".to_string()); query.insert("Format".to_string(), "json".to_string()); query.insert("Version".to_string(), "2017-05-25".to_string()); - query.insert("Timestamp".to_string(), current_aliyun_timestamp()); - query.insert("SignatureNonce".to_string(), new_uuid_simple_string()); - query.insert("SignatureMethod".to_string(), "HMAC-SHA1".to_string()); - query.insert("SignatureVersion".to_string(), "1.0".to_string()); - query.insert( - "AccessKeyId".to_string(), - self.config.access_key_id.clone().unwrap_or_default(), - ); query.insert( "PhoneNumber".to_string(), request.national_phone_number.trim().to_string(), @@ -1080,11 +1063,12 @@ impl AliyunSmsAuthProvider { if let Some(provider_out_id) = request.provider_out_id { query.insert("OutId".to_string(), provider_out_id); } - self.sign_query(&mut query)?; + let signature_headers = self.build_signature_headers("CheckSmsVerifyCode", &query)?; let payload = self .client .post(build_aliyun_sms_url(&self.config.endpoint)?) + .headers(signature_headers) .form(&query) .send() .await @@ -1105,24 +1089,48 @@ impl AliyunSmsAuthProvider { Ok(()) } - fn sign_query(&self, query: &mut BTreeMap) -> Result<(), SmsProviderError> { + fn build_signature_headers( + &self, + action: &str, + form: &BTreeMap, + ) -> Result { + let access_key_id = self.config.access_key_id.as_deref().ok_or_else(|| { + SmsProviderError::InvalidConfig("阿里云短信 AccessKeyId 未配置".to_string()) + })?; let access_key_secret = self.config.access_key_secret.as_deref().ok_or_else(|| { SmsProviderError::InvalidConfig("阿里云短信 AccessKeySecret 未配置".to_string()) })?; - let canonicalized = canonicalize_aliyun_rpc_params(query); - let string_to_sign = format!( - "POST&{}&{}", - aliyun_percent_encode("/"), - aliyun_percent_encode(&canonicalized) + let date = current_aliyun_timestamp(); + let nonce = new_uuid_simple_string(); + let payload = build_aliyun_form_body(form); + let payload_hash = sha256_hex(payload.as_bytes()); + let canonical_headers = format!( + "host:{}\nx-acs-action:{}\nx-acs-content-sha256:{}\nx-acs-date:{}\nx-acs-signature-nonce:{}\nx-acs-version:2017-05-25\n", + self.config.endpoint, action, payload_hash, date, nonce ); - let mut signer = HmacSha1::new_from_slice(format!("{access_key_secret}&").as_bytes()) - .map_err(|error| { - SmsProviderError::InvalidConfig(format!("初始化短信签名器失败:{error}")) - })?; - signer.update(string_to_sign.as_bytes()); - let signature = BASE64_STANDARD.encode(signer.finalize().into_bytes()); - query.insert("Signature".to_string(), signature); - Ok(()) + let signed_headers = + "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version"; + let canonical_request = format!( + "POST\n/\n\n{}\n{}\n{}", + canonical_headers, signed_headers, payload_hash + ); + let string_to_sign = format!( + "ACS3-HMAC-SHA256\n{}", + sha256_hex(canonical_request.as_bytes()) + ); + let signature = hmac_sha256_hex(access_key_secret.as_bytes(), string_to_sign.as_bytes())?; + let authorization = format!( + "ACS3-HMAC-SHA256 Credential={access_key_id},SignedHeaders={signed_headers},Signature={signature}" + ); + let mut headers = reqwest::header::HeaderMap::new(); + insert_header(&mut headers, "x-acs-action", action)?; + insert_header(&mut headers, "x-acs-version", "2017-05-25")?; + insert_header(&mut headers, "x-acs-date", &date)?; + insert_header(&mut headers, "x-acs-signature-nonce", &nonce)?; + insert_header(&mut headers, "x-acs-content-sha256", &payload_hash)?; + insert_header(&mut headers, "authorization", &authorization)?; + + Ok(headers) } } @@ -1453,10 +1461,9 @@ fn current_aliyun_timestamp() -> String { .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()) } -fn canonicalize_aliyun_rpc_params(params: &BTreeMap) -> String { +fn canonicalize_aliyun_form_params(params: &BTreeMap) -> String { params .iter() - .filter(|(key, _)| key.as_str() != "Signature") .map(|(key, value)| { format!( "{}={}", @@ -1468,6 +1475,42 @@ fn canonicalize_aliyun_rpc_params(params: &BTreeMap) -> String { .join("&") } +fn build_aliyun_form_body(params: &BTreeMap) -> String { + serde_urlencoded::to_string(params).unwrap_or_else(|_| canonicalize_aliyun_form_params(params)) +} + +fn hmac_sha256_hex(key: &[u8], content: &[u8]) -> Result { + let mut signer = HmacSha256::new_from_slice(key) + .map_err(|error| SmsProviderError::InvalidConfig(format!("初始化短信签名器失败:{error}")))?; + signer.update(content); + Ok(hex_lower(&signer.finalize().into_bytes())) +} + +fn sha256_hex(content: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(content); + hex_lower(&hasher.finalize()) +} + +fn hex_lower(bytes: &[u8]) -> String { + bytes + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() +} + +fn insert_header( + headers: &mut reqwest::header::HeaderMap, + name: &'static str, + value: &str, +) -> Result<(), SmsProviderError> { + let value = reqwest::header::HeaderValue::from_str(value).map_err(|error| { + SmsProviderError::InvalidConfig(format!("构造阿里云短信签名头失败:{error}")) + })?; + headers.insert(reqwest::header::HeaderName::from_static(name), value); + Ok(()) +} + fn aliyun_percent_encode(value: &str) -> String { urlencoding::encode(value) .into_owned() @@ -2046,7 +2089,7 @@ mod tests { } #[test] - fn canonicalize_aliyun_rpc_params_keeps_sorted_percent_encoded_order() { + fn canonicalize_aliyun_form_params_keeps_sorted_percent_encoded_order() { let mut params = BTreeMap::new(); params.insert( "TemplateParam".to_string(), @@ -2056,11 +2099,53 @@ mod tests { params.insert("PhoneNumber".to_string(), "13800138000".to_string()); assert_eq!( - canonicalize_aliyun_rpc_params(¶ms), + canonicalize_aliyun_form_params(¶ms), "Action=SendSmsVerifyCode&PhoneNumber=13800138000&TemplateParam=%7B%22code%22%3A%22%23%23code%23%23%22%7D" ); } + #[test] + fn aliyun_signature_headers_use_acs3_sha256() { + let config = SmsAuthConfig::new( + SmsAuthProviderKind::Aliyun, + DEFAULT_SMS_ENDPOINT.to_string(), + Some("test-access-key-id".to_string()), + Some("test-access-key-secret".to_string()), + "测试签名".to_string(), + "SMS_001".to_string(), + DEFAULT_SMS_TEMPLATE_PARAM_KEY.to_string(), + DEFAULT_SMS_COUNTRY_CODE.to_string(), + None, + DEFAULT_SMS_CODE_LENGTH, + DEFAULT_SMS_CODE_TYPE, + DEFAULT_SMS_VALID_TIME_SECONDS, + DEFAULT_SMS_INTERVAL_SECONDS, + DEFAULT_SMS_DUPLICATE_POLICY, + DEFAULT_SMS_CASE_AUTH_POLICY, + false, + DEFAULT_SMS_MOCK_VERIFY_CODE.to_string(), + ) + .expect("aliyun config should build"); + let provider = AliyunSmsAuthProvider { + client: Client::new(), + config, + }; + let headers = provider + .build_signature_headers( + "SendSmsVerifyCode", + &BTreeMap::from([("Action".to_string(), "SendSmsVerifyCode".to_string())]), + ) + .expect("signature headers should build"); + + let authorization = headers + .get(reqwest::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .expect("authorization header should exist"); + + assert!(authorization.starts_with("ACS3-HMAC-SHA256 Credential=test-access-key-id")); + assert!(headers.get("x-acs-content-sha256").is_some()); + } + #[test] fn aliyun_send_response_deserializes_pascal_case_fields() { let payload = serde_json::from_str::( diff --git a/server-rs/crates/platform-llm/Cargo.toml b/server-rs/crates/platform-llm/Cargo.toml index 56971711..2fa04d3c 100644 --- a/server-rs/crates/platform-llm/Cargo.toml +++ b/server-rs/crates/platform-llm/Cargo.toml @@ -5,11 +5,11 @@ version.workspace = true license.workspace = true [dependencies] -log.workspace = true -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["time"] } +log = { workspace = true } +reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["time"] } [dev-dependencies] -tokio = { version = "1", features = ["macros", "rt"] } +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/server-rs/crates/platform-oss/Cargo.toml b/server-rs/crates/platform-oss/Cargo.toml index a8b56162..216e5955 100644 --- a/server-rs/crates/platform-oss/Cargo.toml +++ b/server-rs/crates/platform-oss/Cargo.toml @@ -5,14 +5,13 @@ version.workspace = true license.workspace = true [dependencies] -base64 = "0.22" -hmac = "0.12" -httpdate = "1" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -sha1 = "0.10" -time = { version = "0.3", features = ["formatting"] } +base64 = { workspace = true } +hmac = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +time = { workspace = true, features = ["formatting"] } [dev-dependencies] -tokio = { version = "1", features = ["macros", "rt"] } +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/server-rs/crates/platform-oss/src/lib.rs b/server-rs/crates/platform-oss/src/lib.rs index 9d441d27..54c401d6 100644 --- a/server-rs/crates/platform-oss/src/lib.rs +++ b/server-rs/crates/platform-oss/src/lib.rs @@ -1,21 +1,24 @@ -use std::{collections::BTreeMap, error::Error, fmt, time::SystemTime}; +use std::{collections::BTreeMap, error::Error, fmt}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use hmac::{Hmac, Mac}; -use httpdate::fmt_http_date; use reqwest::Method; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use sha1::Sha1; +use sha2::{Digest, Sha256}; use time::{Duration, OffsetDateTime, format_description::well_known::Rfc3339}; -type HmacSha1 = Hmac; +type HmacSha256 = Hmac; pub const DEFAULT_POST_EXPIRE_SECONDS: u64 = 10 * 60; pub const DEFAULT_READ_EXPIRE_SECONDS: u64 = 10 * 60; pub const DEFAULT_POST_MAX_SIZE_BYTES: u64 = 20 * 1024 * 1024; pub const DEFAULT_SUCCESS_ACTION_STATUS: u16 = 200; pub const DEFAULT_METADATA_TOTAL_BYTES_LIMIT: usize = 8 * 1024; +const OSS_V4_ALGORITHM: &str = "OSS4-HMAC-SHA256"; +const OSS_V4_REQUEST: &str = "aliyun_v4_request"; +const OSS_V4_SERVICE: &str = "oss"; +const OSS_UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; pub const LEGACY_PUBLIC_PREFIXES: [&str; 8] = [ "generated-character-drafts", @@ -171,9 +174,13 @@ pub struct OssPutObjectResponse { pub struct OssPostObjectFormFields { pub key: String, pub policy: String, - #[serde(rename = "OSSAccessKeyId")] - pub oss_access_key_id: String, - #[serde(rename = "Signature")] + #[serde(rename = "x-oss-signature-version")] + pub signature_version: String, + #[serde(rename = "x-oss-credential")] + pub credential: String, + #[serde(rename = "x-oss-date")] + pub date: String, + #[serde(rename = "x-oss-signature")] pub signature: String, #[serde(rename = "success_action_status")] pub success_action_status: String, @@ -394,6 +401,10 @@ impl OssClient { .format(&Rfc3339) .map_err(|error| OssError::SerializePolicy(format!("格式化过期时间失败:{error}")))?; + let signed_at = OffsetDateTime::now_utc(); + let signature_scope = build_v4_signature_scope(&self.config.endpoint, signed_at)?; + let signature_date = build_v4_signature_date(signed_at)?; + let credential = format!("{}/{}", self.config.access_key_id, signature_scope); let policy_json = build_policy_json( &self.config.bucket, &object_key, @@ -402,14 +413,17 @@ impl OssClient { success_action_status, content_type.as_deref(), &metadata, + &credential, + &signature_date, ); let policy = serde_json::to_string(&policy_json) .map_err(|error| OssError::SerializePolicy(format!("序列化 policy 失败:{error}")))?; let encoded_policy = BASE64_STANDARD.encode(policy.as_bytes()); - let signature = sign_policy(&self.config.access_key_secret, &encoded_policy)?; + let signature = + sign_v4_content(&self.config.access_key_secret, &signature_scope, &encoded_policy)?; Ok(OssPostObjectResponse { - signature_version: "v1", + signature_version: "v4", provider: "aliyun-oss", bucket: self.config.bucket.clone(), endpoint: self.config.endpoint.clone(), @@ -425,7 +439,9 @@ impl OssClient { form_fields: OssPostObjectFormFields { key: object_key, policy: encoded_policy, - oss_access_key_id: self.config.access_key_id.clone(), + signature_version: OSS_V4_ALGORITHM.to_string(), + credential, + date: signature_date, signature, success_action_status: success_action_status.to_string(), content_type, @@ -458,18 +474,48 @@ impl OssClient { let expires_at_text = expires_at .format(&Rfc3339) .map_err(|error| OssError::Sign(format!("格式化过期时间失败:{error}")))?; - let expires_epoch_seconds = expires_at.unix_timestamp(); - let canonical_resource = build_canonical_object_resource(&self.config.bucket, &object_key); - let string_to_sign = format!("GET\n\n\n{expires_epoch_seconds}\n{canonical_resource}"); - let signature = sign_policy(&self.config.access_key_secret, &string_to_sign)?; + let signed_at = OffsetDateTime::now_utc(); + let signed_at_text = build_v4_signature_date(signed_at)?; + let signature_scope = build_v4_signature_scope(&self.config.endpoint, signed_at)?; + let credential = format!("{}/{}", self.config.access_key_id, signature_scope); + let mut query = BTreeMap::from([ + ("x-oss-additional-headers".to_string(), "host".to_string()), + ( + "x-oss-signature-version".to_string(), + OSS_V4_ALGORITHM.to_string(), + ), + ("x-oss-credential".to_string(), credential), + ("x-oss-date".to_string(), signed_at_text), + ("x-oss-expires".to_string(), expire_seconds.to_string()), + ]); + let canonical_uri = build_v4_canonical_uri(&self.config.bucket, Some(&object_key)); + let object_url_path = format!("/{}", encode_url_path(&object_key)); + let additional_headers = "host"; + let canonical_headers = format!( + "host:{}.{}\n", + self.config.bucket(), + self.config.endpoint() + ); + let canonical_query = build_canonical_query_string(&query); + let canonical_request = build_v4_canonical_request( + Method::GET.as_str(), + &canonical_uri, + &canonical_query, + &canonical_headers, + additional_headers, + OSS_UNSIGNED_PAYLOAD, + ); + let string_to_sign = + build_v4_string_to_sign(query["x-oss-date"].as_str(), &signature_scope, &canonical_request); + let signature = + sign_v4_content(&self.config.access_key_secret, &signature_scope, &string_to_sign)?; + query.insert("x-oss-signature".to_string(), signature); let signed_url = format!( - "{}/{}?OSSAccessKeyId={}&Expires={}&Signature={}", + "{}{}?{}", self.config.upload_host(), - encode_url_path(&object_key), - encode_url_query_value(&self.config.access_key_id), - expires_epoch_seconds, - encode_url_query_value(&signature) + object_url_path, + build_canonical_query_string(&query) ); Ok(OssSignedGetObjectUrlResponse { @@ -656,6 +702,8 @@ fn build_policy_json( success_action_status: u16, content_type: Option<&str>, metadata: &BTreeMap, + credential: &str, + signature_date: &str, ) -> Value { let mut conditions = vec![ json!({ "bucket": bucket }), @@ -666,6 +714,9 @@ fn build_policy_json( "$success_action_status", success_action_status.to_string() ]), + json!(["eq", "$x-oss-signature-version", OSS_V4_ALGORITHM]), + json!(["eq", "$x-oss-credential", credential]), + json!(["eq", "$x-oss-date", signature_date]), ]; if let Some(content_type) = content_type { @@ -695,10 +746,6 @@ fn build_object_url( Ok(url) } -fn build_canonical_object_resource(bucket: &str, object_key: &str) -> String { - format!("/{bucket}/{object_key}") -} - fn build_object_key( prefix: LegacyAssetPrefix, path_segments: &[String], @@ -928,14 +975,6 @@ fn collapse_dashes(value: &str) -> String { .to_string() } -fn sign_policy(access_key_secret: &str, encoded_policy: &str) -> Result { - let mut signer = HmacSha1::new_from_slice(access_key_secret.as_bytes()) - .map_err(|error| OssError::Sign(format!("初始化 HMAC-SHA1 失败:{error}")))?; - signer.update(encoded_policy.as_bytes()); - - Ok(BASE64_STANDARD.encode(signer.finalize().into_bytes())) -} - async fn send_signed_request( client: &reqwest::Client, config: &OssConfig, @@ -966,29 +1005,52 @@ fn signed_request_builder( content_type: Option<&str>, oss_headers: &BTreeMap, ) -> Result { - let date = fmt_http_date(SystemTime::now()); - let canonical_resource = match object_key.map(str::trim).filter(|value| !value.is_empty()) { - Some(object_key) => { - build_canonical_object_resource(config.bucket(), object_key.trim_start_matches('/')) - } - None => format!("/{}/", config.bucket()), - }; - let canonicalized_oss_headers = build_canonicalized_oss_headers(oss_headers); - let string_to_sign = format!( - "{}\n\n{}\n{}\n{}{}", + let signed_at = OffsetDateTime::now_utc(); + let signed_at_text = build_v4_signature_date(signed_at)?; + let signature_scope = build_v4_signature_scope(config.endpoint(), signed_at)?; + let object_path = object_key.map(str::trim).filter(|value| !value.is_empty()); + let canonical_uri = build_v4_canonical_uri(config.bucket(), object_path); + let body_sha256 = OSS_UNSIGNED_PAYLOAD.to_string(); + let mut signed_headers = BTreeMap::from([ + ( + "host".to_string(), + format!("{}.{}", config.bucket(), config.endpoint()), + ), + ("x-oss-content-sha256".to_string(), body_sha256.clone()), + ("x-oss-date".to_string(), signed_at_text.clone()), + ]); + if let Some(content_type) = content_type { + signed_headers.insert("content-type".to_string(), content_type.to_string()); + } + for (key, value) in oss_headers { + signed_headers.insert(key.to_ascii_lowercase(), value.trim().to_string()); + } + + let canonical_headers = build_v4_canonical_headers(&signed_headers); + let additional_headers = "host"; + let canonical_request = build_v4_canonical_request( method.as_str(), - content_type.unwrap_or_default(), - date, - canonicalized_oss_headers, - canonical_resource + &canonical_uri, + "", + &canonical_headers, + additional_headers, + &body_sha256, ); - let signature = sign_policy(config.access_key_secret(), &string_to_sign)?; + let string_to_sign = build_v4_string_to_sign(&signed_at_text, &signature_scope, &canonical_request); + let signature = sign_v4_content(config.access_key_secret(), &signature_scope, &string_to_sign)?; let mut builder = client .request(method, target_url) - .header("Date", date) + .header("x-oss-content-sha256", body_sha256) + .header("x-oss-date", signed_at_text) .header( "Authorization", - format!("OSS {}:{}", config.access_key_id(), signature), + format!( + "{OSS_V4_ALGORITHM} Credential={}/{},AdditionalHeaders={},Signature={}", + config.access_key_id(), + signature_scope, + additional_headers, + signature + ), ); if let Some(content_type) = content_type { @@ -1002,13 +1064,160 @@ fn signed_request_builder( Ok(builder) } -fn build_canonicalized_oss_headers(headers: &BTreeMap) -> String { +fn build_v4_signature_scope(endpoint: &str, signed_at: OffsetDateTime) -> Result { + let date = signed_at + .date() + .to_string() + .replace('-', ""); + let region = extract_oss_region(endpoint)?; + + Ok(format!("{date}/{region}/{OSS_V4_SERVICE}/{OSS_V4_REQUEST}")) +} + +fn build_v4_signature_date(signed_at: OffsetDateTime) -> Result { + let date = signed_at + .date() + .to_string() + .replace('-', ""); + let time = signed_at + .time() + .to_string() + .split('.') + .next() + .unwrap_or("00:00:00") + .replace(':', ""); + + if time.len() != 6 { + return Err(OssError::Sign("OSS V4 签名时间格式化失败".to_string())); + } + + Ok(format!("{date}T{time}Z")) +} + +fn build_v4_canonical_uri(bucket: &str, object_key: Option<&str>) -> String { + match object_key.map(str::trim).filter(|value| !value.is_empty()) { + Some(object_key) => format!( + "/{}/{}", + encode_url_query_value(bucket), + encode_url_path(object_key.trim_start_matches('/')) + ), + None => format!("/{}/", encode_url_query_value(bucket)), + } +} + +fn extract_oss_region(endpoint: &str) -> Result { + endpoint + .trim() + .trim_start_matches("https://") + .trim_start_matches("http://") + .split('.') + .next() + .and_then(|segment| segment.strip_prefix("oss-")) + .map(str::to_string) + .filter(|region| !region.is_empty()) + .ok_or_else(|| { + OssError::InvalidConfig(format!( + "OSS endpoint 无法解析 region,当前值:{endpoint}" + )) + }) +} + +fn sign_v4_content( + access_key_secret: &str, + signature_scope: &str, + content: &str, +) -> Result { + let signing_key = build_v4_signing_key(access_key_secret, signature_scope)?; + Ok(hex_sha256_hmac(&signing_key, content.as_bytes())) +} + +fn build_v4_signing_key(access_key_secret: &str, signature_scope: &str) -> Result, OssError> { + let mut parts = signature_scope.split('/'); + let date = parts + .next() + .ok_or_else(|| OssError::Sign("OSS V4 签名 scope 缺少日期".to_string()))?; + let region = parts + .next() + .ok_or_else(|| OssError::Sign("OSS V4 签名 scope 缺少 region".to_string()))?; + let service = parts + .next() + .ok_or_else(|| OssError::Sign("OSS V4 签名 scope 缺少 service".to_string()))?; + let request = parts + .next() + .ok_or_else(|| OssError::Sign("OSS V4 签名 scope 缺少 request".to_string()))?; + + let date_key = hmac_sha256_raw(format!("aliyun_v4{access_key_secret}").as_bytes(), date)?; + let region_key = hmac_sha256_raw(&date_key, region)?; + let service_key = hmac_sha256_raw(®ion_key, service)?; + hmac_sha256_raw(&service_key, request) +} + +fn hmac_sha256_raw(key: &[u8], content: &str) -> Result, OssError> { + let mut signer = HmacSha256::new_from_slice(key) + .map_err(|error| OssError::Sign(format!("初始化 HMAC-SHA256 失败:{error}")))?; + signer.update(content.as_bytes()); + Ok(signer.finalize().into_bytes().to_vec()) +} + +fn hex_sha256_hmac(key: &[u8], content: &[u8]) -> String { + let mut signer = HmacSha256::new_from_slice(key) + .expect("HMAC-SHA256 accepts keys of any size"); + signer.update(content); + hex_lower(&signer.finalize().into_bytes()) +} + +fn build_v4_canonical_request( + method: &str, + canonical_uri: &str, + canonical_query: &str, + canonical_headers: &str, + signed_headers: &str, + payload_hash: &str, +) -> String { + format!( + "{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n{signed_headers}\n{payload_hash}" + ) +} + +fn build_v4_string_to_sign( + signature_date: &str, + signature_scope: &str, + canonical_request: &str, +) -> String { + format!( + "{OSS_V4_ALGORITHM}\n{signature_date}\n{signature_scope}\n{}", + sha256_hex(canonical_request.as_bytes()) + ) +} + +fn sha256_hex(content: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(content); + hex_lower(&hasher.finalize()) +} + +fn hex_lower(bytes: &[u8]) -> String { + bytes + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() +} + +fn build_v4_canonical_headers(headers: &BTreeMap) -> String { headers .iter() .map(|(key, value)| format!("{}:{}\n", key.to_ascii_lowercase(), value.trim())) .collect::() } +fn build_canonical_query_string(params: &BTreeMap) -> String { + params + .iter() + .map(|(key, value)| format!("{}={}", encode_url_query_value(key), encode_url_query_value(value))) + .collect::>() + .join("&") +} + fn encode_url_path(path: &str) -> String { path.split('/') .map(encode_url_query_value) @@ -1115,8 +1324,20 @@ mod tests { ); assert_eq!(response.bucket, "genarrative-assets".to_string()); assert_eq!( - response.form_fields.oss_access_key_id, - "test-access-key-id".to_string() + response.form_fields.signature_version, + OSS_V4_ALGORITHM.to_string() + ); + assert!(response + .form_fields + .credential + .starts_with("test-access-key-id/")); + assert!(response + .form_fields + .credential + .ends_with("/cn-shanghai/oss/aliyun_v4_request")); + assert_eq!( + response.form_fields.date.len(), + "20260507T120000Z".len() ); assert_eq!( response.form_fields.metadata.get("x-oss-meta-asset-kind"), @@ -1169,6 +1390,18 @@ mod tests { ); assert_eq!( policy["conditions"][4], + json!(["eq", "$x-oss-signature-version", "OSS4-HMAC-SHA256"]) + ); + assert_eq!( + policy["conditions"][5], + json!(["eq", "$x-oss-credential", response.form_fields.credential]) + ); + assert_eq!( + policy["conditions"][6], + json!(["eq", "$x-oss-date", response.form_fields.date]) + ); + assert_eq!( + policy["conditions"][7], json!(["eq", "$content-type", "image/png"]) ); assert_eq!(response.bucket, "genarrative-assets".to_string()); @@ -1206,10 +1439,13 @@ mod tests { assert!( response .signed_url - .contains("OSSAccessKeyId=test-access-key-id") + .contains("x-oss-signature-version=OSS4-HMAC-SHA256") ); - assert!(response.signed_url.contains("&Expires=")); - assert!(response.signed_url.contains("&Signature=")); + assert!(response + .signed_url + .contains("x-oss-credential=test-access-key-id%2F")); + assert!(response.signed_url.contains("&x-oss-expires=300")); + assert!(response.signed_url.contains("&x-oss-signature=")); } #[test] @@ -1282,7 +1518,7 @@ mod tests { } #[test] - fn canonicalized_oss_headers_matches_oss_v1_upload_signature_shape() { + fn canonicalized_oss_headers_matches_sorted_v4_header_shape() { let headers = BTreeMap::from([ ( "x-oss-meta-source-job-id".to_string(), @@ -1295,7 +1531,7 @@ mod tests { ]); assert_eq!( - build_canonicalized_oss_headers(&headers), + build_v4_canonical_headers(&headers), "x-oss-meta-asset-kind:character_visual\nx-oss-meta-source-job-id:job_001\n" ); } diff --git a/server-rs/crates/shared-contracts/Cargo.toml b/server-rs/crates/shared-contracts/Cargo.toml index d2f74a6f..df973184 100644 --- a/server-rs/crates/shared-contracts/Cargo.toml +++ b/server-rs/crates/shared-contracts/Cargo.toml @@ -5,6 +5,6 @@ version.workspace = true license.workspace = true [dependencies] -platform-oss = { path = "../platform-oss" } -serde = { version = "1", features = ["derive"] } -serde_json = "1" +platform-oss = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/server-rs/crates/shared-contracts/src/assets.rs b/server-rs/crates/shared-contracts/src/assets.rs index 8183dfc8..5a66a1c0 100644 --- a/server-rs/crates/shared-contracts/src/assets.rs +++ b/server-rs/crates/shared-contracts/src/assets.rs @@ -525,9 +525,13 @@ pub struct DirectUploadTicketPayload { pub struct DirectUploadTicketFormFields { pub key: String, pub policy: String, - #[serde(rename = "OSSAccessKeyId")] - pub oss_access_key_id: String, - #[serde(rename = "Signature")] + #[serde(rename = "x-oss-signature-version")] + pub signature_version: String, + #[serde(rename = "x-oss-credential")] + pub credential: String, + #[serde(rename = "x-oss-date")] + pub date: String, + #[serde(rename = "x-oss-signature")] pub signature: String, #[serde(rename = "success_action_status")] pub success_action_status: String, @@ -615,7 +619,9 @@ impl From for DirectUploadTicketFormFields { Self { key: value.key, policy: value.policy, - oss_access_key_id: value.oss_access_key_id, + signature_version: value.signature_version, + credential: value.credential, + date: value.date, signature: value.signature, success_action_status: value.success_action_status, content_type: value.content_type, @@ -703,7 +709,7 @@ mod tests { fn direct_upload_ticket_response_keeps_form_fields_shape() { let payload = serde_json::to_value(CreateDirectUploadTicketResponse { upload: DirectUploadTicketPayload::from(OssPostObjectResponse { - signature_version: "v1", + signature_version: "v4", provider: "aliyun-oss", bucket: "genarrative-assets".to_string(), endpoint: "oss-cn-shanghai.aliyuncs.com".to_string(), @@ -719,7 +725,9 @@ mod tests { form_fields: OssPostObjectFormFields { key: "generated-characters/hero/master.png".to_string(), policy: "policy".to_string(), - oss_access_key_id: "ak".to_string(), + signature_version: "OSS4-HMAC-SHA256".to_string(), + credential: "ak/20260507/cn-shanghai/oss/aliyun_v4_request".to_string(), + date: "20260507T120000Z".to_string(), signature: "sig".to_string(), success_action_status: "200".to_string(), content_type: Some("image/png".to_string()), @@ -732,10 +740,14 @@ mod tests { }) .expect("payload should serialize"); - assert_eq!(payload["upload"]["signatureVersion"], json!("v1")); + assert_eq!(payload["upload"]["signatureVersion"], json!("v4")); assert_eq!( - payload["upload"]["formFields"]["OSSAccessKeyId"], - json!("ak") + payload["upload"]["formFields"]["x-oss-signature-version"], + json!("OSS4-HMAC-SHA256") + ); + assert_eq!( + payload["upload"]["formFields"]["x-oss-credential"], + json!("ak/20260507/cn-shanghai/oss/aliyun_v4_request") ); assert_eq!( payload["upload"]["formFields"]["x-oss-meta-asset-kind"], diff --git a/server-rs/crates/shared-kernel/Cargo.toml b/server-rs/crates/shared-kernel/Cargo.toml index f0b0e842..9af9e05b 100644 --- a/server-rs/crates/shared-kernel/Cargo.toml +++ b/server-rs/crates/shared-kernel/Cargo.toml @@ -5,7 +5,7 @@ version.workspace = true license.workspace = true [dependencies] -time = { version = "0.3", features = ["formatting", "parsing"] } +time = { workspace = true, features = ["formatting", "parsing"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -uuid = { version = "1", features = ["v4"] } +uuid = { workspace = true, features = ["v4"] } diff --git a/server-rs/crates/shared-logging/Cargo.toml b/server-rs/crates/shared-logging/Cargo.toml index 141faa54..75235916 100644 --- a/server-rs/crates/shared-logging/Cargo.toml +++ b/server-rs/crates/shared-logging/Cargo.toml @@ -5,4 +5,4 @@ version.workspace = true license.workspace = true [dependencies] -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } diff --git a/server-rs/crates/spacetime-client/Cargo.toml b/server-rs/crates/spacetime-client/Cargo.toml index 4b3203e2..26b0b849 100644 --- a/server-rs/crates/spacetime-client/Cargo.toml +++ b/server-rs/crates/spacetime-client/Cargo.toml @@ -5,23 +5,23 @@ version.workspace = true license.workspace = true [dependencies] -module-ai = { path = "../module-ai" } -module-assets = { path = "../module-assets" } -module-big-fish = { path = "../module-big-fish" } -module-combat = { path = "../module-combat" } -module-custom-world = { path = "../module-custom-world" } -module-inventory = { path = "../module-inventory" } -module-match3d = { path = "../module-match3d" } -module-npc = { path = "../module-npc" } -module-puzzle = { path = "../module-puzzle" } -module-runtime = { path = "../module-runtime" } -module-runtime-story = { path = "../module-runtime-story" } -module-runtime-item = { path = "../module-runtime-item" } -module-square-hole = { path = "../module-square-hole" } -module-story = { path = "../module-story" } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -shared-contracts = { path = "../shared-contracts" } -shared-kernel = { path = "../shared-kernel" } -spacetimedb-sdk = "2.1.0" -tokio = { version = "1", features = ["rt", "sync", "time"] } +module-ai = { workspace = true } +module-assets = { workspace = true } +module-big-fish = { workspace = true } +module-combat = { workspace = true } +module-custom-world = { workspace = true } +module-inventory = { workspace = true } +module-match3d = { workspace = true } +module-npc = { workspace = true } +module-puzzle = { workspace = true } +module-runtime = { workspace = true } +module-runtime-story = { workspace = true } +module-runtime-item = { workspace = true } +module-square-hole = { workspace = true } +module-story = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +shared-contracts = { workspace = true } +shared-kernel = { workspace = true } +spacetimedb-sdk = { workspace = true } +tokio = { workspace = true, features = ["rt", "sync", "time"] } diff --git a/server-rs/crates/spacetime-module/Cargo.toml b/server-rs/crates/spacetime-module/Cargo.toml index 743e0cc2..21377ff7 100644 --- a/server-rs/crates/spacetime-module/Cargo.toml +++ b/server-rs/crates/spacetime-module/Cargo.toml @@ -9,23 +9,23 @@ crate-type = ["cdylib"] [dependencies] log = { workspace = true } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -spacetimedb-lib = { version = "=2.1.0", default-features = false, features = ["serde"] } -module-ai = { path = "../module-ai", default-features = false, features = ["spacetime-types"] } -module-assets = { path = "../module-assets", default-features = false, features = ["spacetime-types"] } -module-big-fish = { path = "../module-big-fish", default-features = false, features = ["spacetime-types"] } -module-combat = { path = "../module-combat", default-features = false, features = ["spacetime-types"] } -module-inventory = { path = "../module-inventory", default-features = false, features = ["spacetime-types"] } -module-custom-world = { path = "../module-custom-world", default-features = false, features = ["spacetime-types"] } -module-match3d = { path = "../module-match3d", default-features = false } -module-npc = { path = "../module-npc", default-features = false, features = ["spacetime-types"] } -module-puzzle = { path = "../module-puzzle", default-features = false, features = ["spacetime-types"] } -module-progression = { path = "../module-progression", default-features = false, features = ["spacetime-types"] } -module-quest = { path = "../module-quest", default-features = false, features = ["spacetime-types"] } -module-runtime = { path = "../module-runtime", default-features = false, features = ["spacetime-types"] } -module-runtime-item = { path = "../module-runtime-item", default-features = false, features = ["spacetime-types"] } -module-square-hole = { path = "../module-square-hole", default-features = false } -module-story = { path = "../module-story", default-features = false, features = ["spacetime-types"] } -shared-kernel = { path = "../shared-kernel" } +serde = { workspace = true } +serde_json = { workspace = true } +module-ai = { workspace = true, features = ["spacetime-types"] } +module-assets = { workspace = true, features = ["spacetime-types"] } +module-big-fish = { workspace = true, features = ["spacetime-types"] } +module-combat = { workspace = true, features = ["spacetime-types"] } +module-inventory = { workspace = true, features = ["spacetime-types"] } +module-custom-world = { workspace = true, features = ["spacetime-types"] } +module-match3d = { workspace = true } +module-npc = { workspace = true, features = ["spacetime-types"] } +module-puzzle = { workspace = true, features = ["spacetime-types"] } +module-progression = { workspace = true, features = ["spacetime-types"] } +module-quest = { workspace = true, features = ["spacetime-types"] } +module-runtime = { workspace = true, features = ["spacetime-types"] } +module-runtime-item = { workspace = true, features = ["spacetime-types"] } +module-square-hole = { workspace = true } +module-story = { workspace = true, features = ["spacetime-types"] } +shared-kernel = { workspace = true } spacetimedb = { workspace = true, features = ["unstable"] } +spacetimedb-lib = { workspace = true, features = ["serde"] } diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 780e328e..b1a82e75 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -1,8 +1,8 @@ use crate::runtime::analytics_date_dimension::analytics_date_dimension; use crate::*; use serde::{Deserialize, Serialize}; -use spacetimedb_lib::sats::de::serde::DeserializeWrapper; -use spacetimedb_lib::sats::ser::serde::SerializeWrapper; +use spacetimedb::sats::de::serde::DeserializeWrapper; +use spacetimedb::sats::ser::serde::SerializeWrapper; use std::collections::HashSet; use crate::big_fish::big_fish_runtime_run;