Consolidate workspace deps and migrate sha1 to sha2
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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 历史残留引用禁止再使用
|
## 2026-05-06 Maincloud 历史残留引用禁止再使用
|
||||||
|
|
||||||
- 背景:项目已经全面移除 Maincloud 运行口径,但历史脚本、测试名和文档仍可能让后续开发误用 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。
|
- 背景:项目已经全面移除 Maincloud 运行口径,但历史脚本、测试名和文档仍可能让后续开发误用 `api-server:maincloud` 或 `GENARRATIVE_SPACETIME_MAINCLOUD_*`。
|
||||||
|
|||||||
@@ -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` / 发布链路。
|
注意:`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 表域总览
|
## SpacetimeDB 表域总览
|
||||||
|
|
||||||
以 `docs/technical/SPACETIMEDB_TABLE_CATALOG.md` 为持续维护入口。当前表域包括:
|
以 `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`
|
- 契约与路由矩阵:`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_SCHEMA_CHANGE_CONSTRAINTS.md`
|
||||||
- SpacetimeDB 表目录:`docs/technical/SPACETIMEDB_TABLE_CATALOG.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`
|
- 生产部署计划:`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`
|
||||||
|
|||||||
@@ -104,9 +104,11 @@
|
|||||||
|
|
||||||
1. 发送验证码调用 `SendSmsVerifyCode`。
|
1. 发送验证码调用 `SendSmsVerifyCode`。
|
||||||
2. 校验验证码调用 `CheckSmsVerifyCode`。
|
2. 校验验证码调用 `CheckSmsVerifyCode`。
|
||||||
3. 使用阿里云 RPC 签名口径:
|
3. 使用阿里云 OpenAPI V3 请求头签名口径:
|
||||||
- `SignatureMethod=HMAC-SHA1`
|
- `Authorization: ACS3-HMAC-SHA256 ...`
|
||||||
- `SignatureVersion=1.0`
|
- `x-acs-action`
|
||||||
|
- `x-acs-version`
|
||||||
|
- `x-acs-content-sha256`
|
||||||
4. 当前仍只支持中国大陆手机号。
|
4. 当前仍只支持中国大陆手机号。
|
||||||
|
|
||||||
## 7. 状态与快照
|
## 7. 状态与快照
|
||||||
|
|||||||
@@ -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 统一通过环境变量注入。
|
- [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`、任务进度、领奖记录和光点钱包流水的边界。
|
- [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):记录方洞挑战结果页图片槽位局部生成、洞口图历史素材、运行态拖拽与点击投放交互的修正口径。
|
- [SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md](./SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.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。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent {
|
agent {
|
||||||
label 'built-in && windows'
|
label 'windows'
|
||||||
}
|
}
|
||||||
|
|
||||||
options {
|
options {
|
||||||
@@ -16,7 +16,7 @@ pipeline {
|
|||||||
CARGO_INCREMENTAL = '0'
|
CARGO_INCREMENTAL = '0'
|
||||||
RUSTC_WRAPPER = 'sccache'
|
RUSTC_WRAPPER = 'sccache'
|
||||||
SCCACHE_DIR = '${env.USERPROFILE}\\.cache\\sccache-stdb-module'
|
SCCACHE_DIR = '${env.USERPROFILE}\\.cache\\sccache-stdb-module'
|
||||||
SCCACHE_CACHE_SIZE = '30G'
|
SCCACHE_CACHE_SIZE = '30G'o
|
||||||
}
|
}
|
||||||
|
|
||||||
parameters {
|
parameters {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
[build]
|
||||||
|
rustc-wrapper = "sccache"
|
||||||
|
|
||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
linker = "clang"
|
linker = "clang"
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
||||||
|
|||||||
65
server-rs/Cargo.lock
generated
65
server-rs/Cargo.lock
generated
@@ -75,7 +75,6 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"hmac",
|
"hmac",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"httpdate",
|
|
||||||
"image",
|
"image",
|
||||||
"module-ai",
|
"module-ai",
|
||||||
"module-assets",
|
"module-assets",
|
||||||
@@ -98,7 +97,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha2",
|
||||||
"shared-contracts",
|
"shared-contracts",
|
||||||
"shared-kernel",
|
"shared-kernel",
|
||||||
"shared-logging",
|
"shared-logging",
|
||||||
@@ -1872,14 +1871,13 @@ name = "platform-auth"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"base64 0.22.1",
|
|
||||||
"hmac",
|
"hmac",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"serde_urlencoded",
|
||||||
"sha2",
|
"sha2",
|
||||||
"shared-kernel",
|
"shared-kernel",
|
||||||
"time",
|
"time",
|
||||||
@@ -1906,11 +1904,10 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"hmac",
|
"hmac",
|
||||||
"httpdate",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha2",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -2730,9 +2727,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb"
|
name = "spacetimedb"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "591f9068644aab6808e7612a869dedde7eeb26df78027a19bc9dc597cc649678"
|
checksum = "1306cc3a9ed9c89f43b263614a529357cc53a067e3d06c1cbb485e3b577b118b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -2753,9 +2750,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-bindings-macro"
|
name = "spacetimedb-bindings-macro"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f68bf4810d838be622c13efd4cd64e0a9ce8cd340deaa730f0c92caee845f9"
|
checksum = "51567ec01cd323438a00c134c16f26ffcde5f9dbe6a42a52e54578285bf49d73"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"humantime",
|
"humantime",
|
||||||
@@ -2767,18 +2764,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-bindings-sys"
|
name = "spacetimedb-bindings-sys"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c2fe9f4124a599c9deae8f8231be3ae5a49bc5b2eef5e04c04b2632cf4cc0b4"
|
checksum = "3b40fa1bea26664085febe2b4455568c8b47dea2cb0245406b27e30963df2ba1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"spacetimedb-primitives",
|
"spacetimedb-primitives",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-client-api-messages"
|
name = "spacetimedb-client-api-messages"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02a18c2e145f61ad498f8094a2231e09f8d39a3dde09defa716075dbcb8c7e85"
|
checksum = "dfc9eeb20a555bad07029cbee4efe3a305cb5c1e40e21a07cbbbbed16a106014"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"bytestring",
|
"bytestring",
|
||||||
@@ -2798,9 +2795,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-data-structures"
|
name = "spacetimedb-data-structures"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e4035c17ddbfc8c49a659bd6fb265b0a2a11115d1b4ad1963bccfad75cdfb4b"
|
checksum = "748fd5850a757823c5b8948065d9e4dc5092968a051aa3f34f170e91d95e493b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
@@ -2813,9 +2810,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-lib"
|
name = "spacetimedb-lib"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "672c0dd16feced67155a0dee7bd38d30f7725321c8177cb871a21c3d8749ae97"
|
checksum = "5612611d09d358f535438275d2a0d6a5e2fa56fa583dcfdbeddd623974df1d5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
@@ -2837,9 +2834,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-memory-usage"
|
name = "spacetimedb-memory-usage"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c00614eb981354ee6b31661ec47002d3fc274f9d4543279dd6ee8692cdd8266"
|
checksum = "1c3a0d08fc5d8688a47e3ffcb803275519663b7ea1fba7ad25e608182de4ec6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decorum",
|
"decorum",
|
||||||
"ethnum",
|
"ethnum",
|
||||||
@@ -2847,9 +2844,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-metrics"
|
name = "spacetimedb-metrics"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef6f7f6b24932505a696b75b7e5e60646ab1d76eeb8d2f95f04948562c965b5e"
|
checksum = "ca2d647201339aa17ba438a07463e96ed64ba214fb0c182588e262b055efa7f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -2859,9 +2856,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-primitives"
|
name = "spacetimedb-primitives"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dba5d7497d54aa8d4254f78a0bef12606bb05e62f8dea8b69abc9b241508e8b7"
|
checksum = "9b668b51e7318207ae7eebcd4cae0c5d43bf713e7f229ac309ea2614a486ffde"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"either",
|
"either",
|
||||||
@@ -2873,18 +2870,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-query-builder"
|
name = "spacetimedb-query-builder"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d04c6e41e05273f14405ac6f429477626677d46528b561a509b7b78b45128f30"
|
checksum = "0186b1a2b3bf25bdd0f2676b61801fd754013ca6a58e1e24cc5148945388bc9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"spacetimedb-lib",
|
"spacetimedb-lib",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-sats"
|
name = "spacetimedb-sats"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfde33ec86d80881da8b00c42096bf0382bef8e1bc35e9b6faaa42d77cbf503c"
|
checksum = "11780ed69f178bf3784b7599da5171450e4b7ac6fd66b79e2e1861c867cef1a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
@@ -2915,9 +2912,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-schema"
|
name = "spacetimedb-schema"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b03b34a38bd39f3f60a0687efafb942355bd9f6026b88a38c7c9ec904e944f1"
|
checksum = "1e4e9f8aa596e0e7034f0c8b3649d3fa3cc7bde340761519c3a3c60f10ec8888"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"convert_case 0.6.0",
|
"convert_case 0.6.0",
|
||||||
@@ -2946,9 +2943,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-sdk"
|
name = "spacetimedb-sdk"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e7302851fec72929ffef976125f51971b0ec76be2730e27705a8e544f2ce159"
|
checksum = "41e82f20034b8aaeaa081871b07895aab45be1f0fc35e114ab64ae8e7e5c1a54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anymap3",
|
"anymap3",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
@@ -2978,9 +2975,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacetimedb-sql-parser"
|
name = "spacetimedb-sql-parser"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7cbb9a837ac5f1ddb0cfb745159dea276dcf456244452f5a90684e5184f1f31"
|
checksum = "ec5c77a2d4e3f42ede59598c56cb81a0fe54fd1974e2707f7140d1d5f41d08a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"spacetimedb-lib",
|
"spacetimedb-lib",
|
||||||
|
|||||||
@@ -42,8 +42,62 @@ version = "0.1.0"
|
|||||||
license = "UNLICENSED"
|
license = "UNLICENSED"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[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"
|
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]
|
[profile.dev]
|
||||||
opt-level = 0 # 默认 0,有人手滑改 1/2 会慢
|
opt-level = 0 # 默认 0,有人手滑改 1/2 会慢
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
## 2. 当前阶段说明
|
## 2. 当前阶段说明
|
||||||
|
|
||||||
当前目录已经完成以下三十七项初始化:
|
当前目录已经完成以下三十八项初始化:
|
||||||
|
|
||||||
1. 为新后端预留正式目录并把路径固定到仓库结构中。
|
1. 为新后端预留正式目录并把路径固定到仓库结构中。
|
||||||
2. 创建虚拟 workspace `Cargo.toml`,后续 crate 会逐项挂入。
|
2. 创建虚拟 workspace `Cargo.toml`,后续 crate 会逐项挂入。
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
35. 创建 `scripts/spacetime-dev.sh`,固定 Unix-like 本地 SpacetimeDB 启动入口。
|
35. 创建 `scripts/spacetime-dev.sh`,固定 Unix-like 本地 SpacetimeDB 启动入口。
|
||||||
36. 创建 `scripts/oss-smoke.ps1`,固定 Windows 本地阿里云 OSS 真实联调入口。
|
36. 创建 `scripts/oss-smoke.ps1`,固定 Windows 本地阿里云 OSS 真实联调入口。
|
||||||
37. 固定 Vite dev proxy 的 Rust `api-server` 默认目标与 `GENARRATIVE_RUNTIME_SERVER_TARGET` 覆盖开关。
|
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`。
|
4. `spacetime-module` 新增业务入口前先确认是否已有对应上下文目录,禁止继续把大段业务流程堆回 `src/lib.rs`。
|
||||||
5. 根目录可执行 `npm run check:server-rs-ddd` 检查第一阶段 DDD 骨架与绝对边界。
|
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. 关联文档
|
## 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)
|
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)
|
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)
|
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)
|
||||||
|
|||||||
@@ -5,51 +5,50 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-stream = "0.3"
|
async-stream = { workspace = true }
|
||||||
axum = "0.8"
|
axum = { workspace = true }
|
||||||
base64 = "0.22"
|
base64 = { workspace = true }
|
||||||
dotenvy = "0.15"
|
dotenvy = { workspace = true }
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
|
image = { workspace = true, features = ["jpeg", "png", "webp"] }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||||
webp = "0.3"
|
webp = { workspace = true }
|
||||||
module-ai = { path = "../module-ai" }
|
module-ai = { workspace = true }
|
||||||
module-assets = { path = "../module-assets" }
|
module-assets = { workspace = true, features = ["server-service"] }
|
||||||
module-auth = { path = "../module-auth" }
|
module-auth = { workspace = true }
|
||||||
module-big-fish = { path = "../module-big-fish" }
|
module-big-fish = { workspace = true }
|
||||||
module-combat = { path = "../module-combat" }
|
module-combat = { workspace = true }
|
||||||
module-custom-world = { path = "../module-custom-world" }
|
module-custom-world = { workspace = true }
|
||||||
module-inventory = { path = "../module-inventory" }
|
module-inventory = { workspace = true }
|
||||||
module-match3d = { path = "../module-match3d" }
|
module-match3d = { workspace = true }
|
||||||
module-npc = { path = "../module-npc" }
|
module-npc = { workspace = true }
|
||||||
module-puzzle = { path = "../module-puzzle" }
|
module-puzzle = { workspace = true }
|
||||||
module-runtime = { path = "../module-runtime" }
|
module-runtime = { workspace = true }
|
||||||
module-runtime-story = { path = "../module-runtime-story" }
|
module-runtime-story = { workspace = true }
|
||||||
module-runtime-item = { path = "../module-runtime-item" }
|
module-runtime-item = { workspace = true }
|
||||||
module-square-hole = { path = "../module-square-hole" }
|
module-square-hole = { workspace = true }
|
||||||
module-story = { path = "../module-story" }
|
module-story = { workspace = true }
|
||||||
platform-auth = { path = "../platform-auth" }
|
platform-auth = { workspace = true }
|
||||||
platform-llm = { path = "../platform-llm" }
|
platform-llm = { workspace = true }
|
||||||
platform-oss = { path = "../platform-oss" }
|
platform-oss = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
shared-contracts = { path = "../shared-contracts" }
|
shared-contracts = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
shared-logging = { path = "../shared-logging" }
|
shared-logging = { workspace = true }
|
||||||
spacetime-client = { path = "../spacetime-client" }
|
spacetime-client = { workspace = true }
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "time"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = { workspace = true }
|
||||||
time = { version = "0.3", features = ["formatting"] }
|
time = { workspace = true, features = ["formatting"] }
|
||||||
tower-http = { version = "0.6", features = ["trace"] }
|
tower-http = { workspace = true, features = ["trace"] }
|
||||||
tracing = "0.1"
|
tracing = { workspace = true }
|
||||||
url = "2"
|
url = { workspace = true }
|
||||||
urlencoding = "2"
|
urlencoding = { workspace = true }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
base64 = "0.22"
|
base64 = { workspace = true }
|
||||||
hmac = "0.12"
|
hmac = { workspace = true }
|
||||||
httpdate = "1"
|
http-body-util = { workspace = true }
|
||||||
http-body-util = "0.1"
|
reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls"] }
|
sha2 = { workspace = true }
|
||||||
sha1 = "0.10"
|
tower = { workspace = true, features = ["util"] }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
|
||||||
|
|||||||
@@ -473,26 +473,23 @@ mod tests {
|
|||||||
error::Error,
|
error::Error,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::SystemTime,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
};
|
};
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use httpdate::fmt_http_date;
|
|
||||||
use reqwest::{Method, multipart};
|
use reqwest::{Method, multipart};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use sha1::Sha1;
|
use sha2::{Digest, Sha256};
|
||||||
use shared_kernel::new_uuid_simple_string;
|
use shared_kernel::new_uuid_simple_string;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
use crate::{app::build_router, config::AppConfig, state::AppState};
|
use crate::{app::build_router, config::AppConfig, state::AppState};
|
||||||
|
|
||||||
type HmacSha1 = Hmac<Sha1>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn asset_history_kind_support_includes_puzzle_cover_image() {
|
fn asset_history_kind_support_includes_puzzle_cover_image() {
|
||||||
@@ -653,8 +650,13 @@ mod tests {
|
|||||||
Value::String("private".to_string())
|
Value::String("private".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["data"]["upload"]["formFields"]["OSSAccessKeyId"],
|
payload["data"]["upload"]["formFields"]["x-oss-signature-version"],
|
||||||
Value::String("test-access-key-id".to_string())
|
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());
|
assert!(payload["data"]["upload"].get("publicUrl").is_none());
|
||||||
}
|
}
|
||||||
@@ -702,7 +704,7 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
payload["data"]["read"]["signedUrl"]
|
payload["data"]["read"]["signedUrl"]
|
||||||
.as_str()
|
.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
|
.oss_access_key_secret
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| std::io::Error::other("缺少 oss access key secret"))?;
|
.ok_or_else(|| std::io::Error::other("缺少 oss access key secret"))?;
|
||||||
let date = fmt_http_date(SystemTime::now());
|
let signed_at = time::OffsetDateTime::now_utc();
|
||||||
let canonical_resource = match object_key.map(str::trim).filter(|value| !value.is_empty()) {
|
let signed_at_text = build_oss_v4_signature_date(signed_at);
|
||||||
Some(object_key) => format!("/{bucket}/{}", object_key.trim_start_matches('/')),
|
let signature_scope = build_oss_v4_signature_scope(endpoint, signed_at)?;
|
||||||
None => format!("/{bucket}/"),
|
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 string_to_sign = format!("{}\n\n\n{}\n{}", method.as_str(), date, canonical_resource);
|
let payload_hash = "UNSIGNED-PAYLOAD";
|
||||||
let signature = sign_oss_string(access_key_secret, &string_to_sign)?;
|
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()) {
|
let target_url = match object_key.map(str::trim).filter(|value| !value.is_empty()) {
|
||||||
Some(object_key) => build_object_url(config, object_key)?,
|
Some(object_key) => build_object_url(config, object_key)?,
|
||||||
None => reqwest::Url::parse(&format!("https://{bucket}.{endpoint}/"))?,
|
None => reqwest::Url::parse(&format!("https://{bucket}.{endpoint}/"))?,
|
||||||
@@ -1424,18 +1439,147 @@ mod tests {
|
|||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.request(method, target_url)
|
.request(method, target_url)
|
||||||
.header("Date", date)
|
.header("x-oss-content-sha256", payload_hash)
|
||||||
.header("Authorization", format!("OSS {access_key_id}:{signature}"))
|
.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()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_oss_string(secret: &str, content: &str) -> Result<String, Box<dyn Error>> {
|
fn build_oss_v4_signature_scope(
|
||||||
let mut signer = HmacSha1::new_from_slice(secret.as_bytes())?;
|
endpoint: &str,
|
||||||
|
signed_at: time::OffsetDateTime,
|
||||||
|
) -> Result<String, Box<dyn Error>> {
|
||||||
|
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<String, Box<dyn Error>> {
|
||||||
|
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());
|
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<Vec<u8>, Box<dyn Error>> {
|
||||||
|
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<Vec<u8>, Box<dyn Error>> {
|
||||||
|
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::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_oss_url_path(path: &str) -> String {
|
||||||
|
path.split('/')
|
||||||
|
.map(encode_oss_url_query_value)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.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<dyn Error>> {
|
fn ensure_success_status(status: u16, message: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ server-service = ["dep:platform-oss", "dep:reqwest"]
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"], optional = true }
|
reqwest = { workspace = true, features = ["rustls-tls"], optional = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
platform-oss = { path = "../platform-oss", optional = true }
|
platform-oss = { workspace = true, optional = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
platform-auth = { path = "../platform-auth" }
|
platform-auth = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
time = { workspace = true, features = ["formatting", "parsing"] }
|
||||||
tracing = "0.1"
|
tracing = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["macros", "rt"] }
|
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb", "module-runtime-item/spacetime-types"]
|
spacetime-types = ["dep:spacetimedb", "module-runtime-item/spacetime-types"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
module-runtime-item = { path = "../module-runtime-item", default-features = false }
|
module-runtime-item = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
module-inventory = { path = "../module-inventory", default-features = false }
|
module-inventory = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
shared-contracts = { path = "../shared-contracts" }
|
shared-contracts = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
time = { version = "0.3", features = ["formatting"] }
|
time = { workspace = true, features = ["formatting"] }
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
time = { workspace = true, features = ["formatting", "parsing"] }
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ default = []
|
|||||||
spacetime-types = ["dep:spacetimedb"]
|
spacetime-types = ["dep:spacetimedb"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ spacetime-types = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
module-combat = { path = "../module-combat", default-features = false }
|
module-combat = { workspace = true }
|
||||||
module-inventory = { path = "../module-inventory", default-features = false }
|
module-inventory = { workspace = true }
|
||||||
module-progression = { path = "../module-progression", default-features = false }
|
module-progression = { workspace = true }
|
||||||
module-quest = { path = "../module-quest", default-features = false }
|
module-quest = { workspace = true }
|
||||||
module-runtime-item = { path = "../module-runtime-item", default-features = false }
|
module-runtime-item = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb = { workspace = true, optional = true }
|
spacetimedb = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -5,21 +5,20 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = "0.5"
|
argon2 = { workspace = true }
|
||||||
base64 = "0.22"
|
hmac = { workspace = true }
|
||||||
hmac = "0.12"
|
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
serde_json = { workspace = true }
|
||||||
serde_json = "1"
|
serde_urlencoded = { workspace = true }
|
||||||
sha1 = "0.10"
|
sha2 = { workspace = true }
|
||||||
sha2 = "0.10"
|
jsonwebtoken = { workspace = true }
|
||||||
jsonwebtoken = "9"
|
rand_core = { workspace = true, features = ["getrandom"] }
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
serde = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
shared-kernel = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
time = { workspace = true, features = ["std"] }
|
||||||
time = { version = "0.3", features = ["std"] }
|
tracing = { workspace = true }
|
||||||
tracing = "0.1"
|
url = { workspace = true }
|
||||||
url = "2"
|
urlencoding = { workspace = true }
|
||||||
urlencoding = "2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["macros", "rt"] }
|
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
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 hmac::{Hmac, Mac};
|
||||||
use jsonwebtoken::{
|
use jsonwebtoken::{
|
||||||
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind,
|
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind,
|
||||||
@@ -14,7 +13,6 @@ use rand_core::OsRng;
|
|||||||
use reqwest::{Client, StatusCode};
|
use reqwest::{Client, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha1::Sha1;
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use shared_kernel::{new_uuid_simple_string, normalize_optional_string, normalize_required_string};
|
use shared_kernel::{new_uuid_simple_string, normalize_optional_string, normalize_required_string};
|
||||||
use time::{Duration, OffsetDateTime};
|
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";
|
"https://api.weixin.qq.com/sns/oauth2/access_token";
|
||||||
pub const DEFAULT_WECHAT_USER_INFO_ENDPOINT: &str = "https://api.weixin.qq.com/sns/userinfo";
|
pub const DEFAULT_WECHAT_USER_INFO_ENDPOINT: &str = "https://api.weixin.qq.com/sns/userinfo";
|
||||||
|
|
||||||
type HmacSha1 = Hmac<Sha1>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
// 鉴权 provider 直接冻结成文档中约定的枚举,避免后续在多个 crate 内重复发明字符串字面量。
|
// 鉴权 provider 直接冻结成文档中约定的枚举,避免后续在多个 crate 内重复发明字符串字面量。
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -927,14 +925,6 @@ impl AliyunSmsAuthProvider {
|
|||||||
query.insert("Action".to_string(), "SendSmsVerifyCode".to_string());
|
query.insert("Action".to_string(), "SendSmsVerifyCode".to_string());
|
||||||
query.insert("Format".to_string(), "json".to_string());
|
query.insert("Format".to_string(), "json".to_string());
|
||||||
query.insert("Version".to_string(), "2017-05-25".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(
|
query.insert(
|
||||||
"PhoneNumber".to_string(),
|
"PhoneNumber".to_string(),
|
||||||
request.national_phone_number.trim().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() {
|
if let Some(scheme_name) = self.config.scheme_name.clone() {
|
||||||
query.insert("SchemeName".to_string(), scheme_name);
|
query.insert("SchemeName".to_string(), scheme_name);
|
||||||
}
|
}
|
||||||
self.sign_query(&mut query)?;
|
let signature_headers = self.build_signature_headers("SendSmsVerifyCode", &query)?;
|
||||||
|
|
||||||
let payload = self
|
let payload = self
|
||||||
.client
|
.client
|
||||||
.post(build_aliyun_sms_url(&self.config.endpoint)?)
|
.post(build_aliyun_sms_url(&self.config.endpoint)?)
|
||||||
|
.headers(signature_headers)
|
||||||
.form(&query)
|
.form(&query)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -1053,14 +1044,6 @@ impl AliyunSmsAuthProvider {
|
|||||||
query.insert("Action".to_string(), "CheckSmsVerifyCode".to_string());
|
query.insert("Action".to_string(), "CheckSmsVerifyCode".to_string());
|
||||||
query.insert("Format".to_string(), "json".to_string());
|
query.insert("Format".to_string(), "json".to_string());
|
||||||
query.insert("Version".to_string(), "2017-05-25".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(
|
query.insert(
|
||||||
"PhoneNumber".to_string(),
|
"PhoneNumber".to_string(),
|
||||||
request.national_phone_number.trim().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 {
|
if let Some(provider_out_id) = request.provider_out_id {
|
||||||
query.insert("OutId".to_string(), 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
|
let payload = self
|
||||||
.client
|
.client
|
||||||
.post(build_aliyun_sms_url(&self.config.endpoint)?)
|
.post(build_aliyun_sms_url(&self.config.endpoint)?)
|
||||||
|
.headers(signature_headers)
|
||||||
.form(&query)
|
.form(&query)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -1105,24 +1089,48 @@ impl AliyunSmsAuthProvider {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_query(&self, query: &mut BTreeMap<String, String>) -> Result<(), SmsProviderError> {
|
fn build_signature_headers(
|
||||||
|
&self,
|
||||||
|
action: &str,
|
||||||
|
form: &BTreeMap<String, String>,
|
||||||
|
) -> Result<reqwest::header::HeaderMap, SmsProviderError> {
|
||||||
|
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(|| {
|
let access_key_secret = self.config.access_key_secret.as_deref().ok_or_else(|| {
|
||||||
SmsProviderError::InvalidConfig("阿里云短信 AccessKeySecret 未配置".to_string())
|
SmsProviderError::InvalidConfig("阿里云短信 AccessKeySecret 未配置".to_string())
|
||||||
})?;
|
})?;
|
||||||
let canonicalized = canonicalize_aliyun_rpc_params(query);
|
let date = current_aliyun_timestamp();
|
||||||
let string_to_sign = format!(
|
let nonce = new_uuid_simple_string();
|
||||||
"POST&{}&{}",
|
let payload = build_aliyun_form_body(form);
|
||||||
aliyun_percent_encode("/"),
|
let payload_hash = sha256_hex(payload.as_bytes());
|
||||||
aliyun_percent_encode(&canonicalized)
|
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())
|
let signed_headers =
|
||||||
.map_err(|error| {
|
"host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
|
||||||
SmsProviderError::InvalidConfig(format!("初始化短信签名器失败:{error}"))
|
let canonical_request = format!(
|
||||||
})?;
|
"POST\n/\n\n{}\n{}\n{}",
|
||||||
signer.update(string_to_sign.as_bytes());
|
canonical_headers, signed_headers, payload_hash
|
||||||
let signature = BASE64_STANDARD.encode(signer.finalize().into_bytes());
|
);
|
||||||
query.insert("Signature".to_string(), signature);
|
let string_to_sign = format!(
|
||||||
Ok(())
|
"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())
|
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize_aliyun_rpc_params(params: &BTreeMap<String, String>) -> String {
|
fn canonicalize_aliyun_form_params(params: &BTreeMap<String, String>) -> String {
|
||||||
params
|
params
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(key, _)| key.as_str() != "Signature")
|
|
||||||
.map(|(key, value)| {
|
.map(|(key, value)| {
|
||||||
format!(
|
format!(
|
||||||
"{}={}",
|
"{}={}",
|
||||||
@@ -1468,6 +1475,42 @@ fn canonicalize_aliyun_rpc_params(params: &BTreeMap<String, String>) -> String {
|
|||||||
.join("&")
|
.join("&")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_aliyun_form_body(params: &BTreeMap<String, String>) -> String {
|
||||||
|
serde_urlencoded::to_string(params).unwrap_or_else(|_| canonicalize_aliyun_form_params(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hmac_sha256_hex(key: &[u8], content: &[u8]) -> Result<String, SmsProviderError> {
|
||||||
|
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::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
fn aliyun_percent_encode(value: &str) -> String {
|
||||||
urlencoding::encode(value)
|
urlencoding::encode(value)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
@@ -2046,7 +2089,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let mut params = BTreeMap::new();
|
||||||
params.insert(
|
params.insert(
|
||||||
"TemplateParam".to_string(),
|
"TemplateParam".to_string(),
|
||||||
@@ -2056,11 +2099,53 @@ mod tests {
|
|||||||
params.insert("PhoneNumber".to_string(), "13800138000".to_string());
|
params.insert("PhoneNumber".to_string(), "13800138000".to_string());
|
||||||
|
|
||||||
assert_eq!(
|
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"
|
"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]
|
#[test]
|
||||||
fn aliyun_send_response_deserializes_pascal_case_fields() {
|
fn aliyun_send_response_deserializes_pascal_case_fields() {
|
||||||
let payload = serde_json::from_str::<AliyunSendSmsVerifyCodeResponse>(
|
let payload = serde_json::from_str::<AliyunSendSmsVerifyCodeResponse>(
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log.workspace = true
|
log = { workspace = true }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
|
reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
tokio = { version = "1", features = ["time"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["macros", "rt"] }
|
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22"
|
base64 = { workspace = true }
|
||||||
hmac = "0.12"
|
hmac = { workspace = true }
|
||||||
httpdate = "1"
|
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
serde = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde_json = { workspace = true }
|
||||||
serde_json = "1"
|
sha2 = { workspace = true }
|
||||||
sha1 = "0.10"
|
time = { workspace = true, features = ["formatting"] }
|
||||||
time = { version = "0.3", features = ["formatting"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["macros", "rt"] }
|
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||||
|
|||||||
@@ -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 base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use httpdate::fmt_http_date;
|
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use sha1::Sha1;
|
use sha2::{Digest, Sha256};
|
||||||
use time::{Duration, OffsetDateTime, format_description::well_known::Rfc3339};
|
use time::{Duration, OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
|
|
||||||
type HmacSha1 = Hmac<Sha1>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
pub const DEFAULT_POST_EXPIRE_SECONDS: u64 = 10 * 60;
|
pub const DEFAULT_POST_EXPIRE_SECONDS: u64 = 10 * 60;
|
||||||
pub const DEFAULT_READ_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_POST_MAX_SIZE_BYTES: u64 = 20 * 1024 * 1024;
|
||||||
pub const DEFAULT_SUCCESS_ACTION_STATUS: u16 = 200;
|
pub const DEFAULT_SUCCESS_ACTION_STATUS: u16 = 200;
|
||||||
pub const DEFAULT_METADATA_TOTAL_BYTES_LIMIT: usize = 8 * 1024;
|
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] = [
|
pub const LEGACY_PUBLIC_PREFIXES: [&str; 8] = [
|
||||||
"generated-character-drafts",
|
"generated-character-drafts",
|
||||||
@@ -171,9 +174,13 @@ pub struct OssPutObjectResponse {
|
|||||||
pub struct OssPostObjectFormFields {
|
pub struct OssPostObjectFormFields {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub policy: String,
|
pub policy: String,
|
||||||
#[serde(rename = "OSSAccessKeyId")]
|
#[serde(rename = "x-oss-signature-version")]
|
||||||
pub oss_access_key_id: String,
|
pub signature_version: String,
|
||||||
#[serde(rename = "Signature")]
|
#[serde(rename = "x-oss-credential")]
|
||||||
|
pub credential: String,
|
||||||
|
#[serde(rename = "x-oss-date")]
|
||||||
|
pub date: String,
|
||||||
|
#[serde(rename = "x-oss-signature")]
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
#[serde(rename = "success_action_status")]
|
#[serde(rename = "success_action_status")]
|
||||||
pub success_action_status: String,
|
pub success_action_status: String,
|
||||||
@@ -394,6 +401,10 @@ impl OssClient {
|
|||||||
.format(&Rfc3339)
|
.format(&Rfc3339)
|
||||||
.map_err(|error| OssError::SerializePolicy(format!("格式化过期时间失败:{error}")))?;
|
.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(
|
let policy_json = build_policy_json(
|
||||||
&self.config.bucket,
|
&self.config.bucket,
|
||||||
&object_key,
|
&object_key,
|
||||||
@@ -402,14 +413,17 @@ impl OssClient {
|
|||||||
success_action_status,
|
success_action_status,
|
||||||
content_type.as_deref(),
|
content_type.as_deref(),
|
||||||
&metadata,
|
&metadata,
|
||||||
|
&credential,
|
||||||
|
&signature_date,
|
||||||
);
|
);
|
||||||
let policy = serde_json::to_string(&policy_json)
|
let policy = serde_json::to_string(&policy_json)
|
||||||
.map_err(|error| OssError::SerializePolicy(format!("序列化 policy 失败:{error}")))?;
|
.map_err(|error| OssError::SerializePolicy(format!("序列化 policy 失败:{error}")))?;
|
||||||
let encoded_policy = BASE64_STANDARD.encode(policy.as_bytes());
|
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 {
|
Ok(OssPostObjectResponse {
|
||||||
signature_version: "v1",
|
signature_version: "v4",
|
||||||
provider: "aliyun-oss",
|
provider: "aliyun-oss",
|
||||||
bucket: self.config.bucket.clone(),
|
bucket: self.config.bucket.clone(),
|
||||||
endpoint: self.config.endpoint.clone(),
|
endpoint: self.config.endpoint.clone(),
|
||||||
@@ -425,7 +439,9 @@ impl OssClient {
|
|||||||
form_fields: OssPostObjectFormFields {
|
form_fields: OssPostObjectFormFields {
|
||||||
key: object_key,
|
key: object_key,
|
||||||
policy: encoded_policy,
|
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,
|
signature,
|
||||||
success_action_status: success_action_status.to_string(),
|
success_action_status: success_action_status.to_string(),
|
||||||
content_type,
|
content_type,
|
||||||
@@ -458,18 +474,48 @@ impl OssClient {
|
|||||||
let expires_at_text = expires_at
|
let expires_at_text = expires_at
|
||||||
.format(&Rfc3339)
|
.format(&Rfc3339)
|
||||||
.map_err(|error| OssError::Sign(format!("格式化过期时间失败:{error}")))?;
|
.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 signed_at = OffsetDateTime::now_utc();
|
||||||
let string_to_sign = format!("GET\n\n\n{expires_epoch_seconds}\n{canonical_resource}");
|
let signed_at_text = build_v4_signature_date(signed_at)?;
|
||||||
let signature = sign_policy(&self.config.access_key_secret, &string_to_sign)?;
|
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!(
|
let signed_url = format!(
|
||||||
"{}/{}?OSSAccessKeyId={}&Expires={}&Signature={}",
|
"{}{}?{}",
|
||||||
self.config.upload_host(),
|
self.config.upload_host(),
|
||||||
encode_url_path(&object_key),
|
object_url_path,
|
||||||
encode_url_query_value(&self.config.access_key_id),
|
build_canonical_query_string(&query)
|
||||||
expires_epoch_seconds,
|
|
||||||
encode_url_query_value(&signature)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(OssSignedGetObjectUrlResponse {
|
Ok(OssSignedGetObjectUrlResponse {
|
||||||
@@ -656,6 +702,8 @@ fn build_policy_json(
|
|||||||
success_action_status: u16,
|
success_action_status: u16,
|
||||||
content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
|
credential: &str,
|
||||||
|
signature_date: &str,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let mut conditions = vec![
|
let mut conditions = vec![
|
||||||
json!({ "bucket": bucket }),
|
json!({ "bucket": bucket }),
|
||||||
@@ -666,6 +714,9 @@ fn build_policy_json(
|
|||||||
"$success_action_status",
|
"$success_action_status",
|
||||||
success_action_status.to_string()
|
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 {
|
if let Some(content_type) = content_type {
|
||||||
@@ -695,10 +746,6 @@ fn build_object_url(
|
|||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_canonical_object_resource(bucket: &str, object_key: &str) -> String {
|
|
||||||
format!("/{bucket}/{object_key}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_object_key(
|
fn build_object_key(
|
||||||
prefix: LegacyAssetPrefix,
|
prefix: LegacyAssetPrefix,
|
||||||
path_segments: &[String],
|
path_segments: &[String],
|
||||||
@@ -928,14 +975,6 @@ fn collapse_dashes(value: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_policy(access_key_secret: &str, encoded_policy: &str) -> Result<String, OssError> {
|
|
||||||
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(
|
async fn send_signed_request(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
config: &OssConfig,
|
config: &OssConfig,
|
||||||
@@ -966,29 +1005,52 @@ fn signed_request_builder(
|
|||||||
content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
oss_headers: &BTreeMap<String, String>,
|
oss_headers: &BTreeMap<String, String>,
|
||||||
) -> Result<reqwest::RequestBuilder, OssError> {
|
) -> Result<reqwest::RequestBuilder, OssError> {
|
||||||
let date = fmt_http_date(SystemTime::now());
|
let signed_at = OffsetDateTime::now_utc();
|
||||||
let canonical_resource = match object_key.map(str::trim).filter(|value| !value.is_empty()) {
|
let signed_at_text = build_v4_signature_date(signed_at)?;
|
||||||
Some(object_key) => {
|
let signature_scope = build_v4_signature_scope(config.endpoint(), signed_at)?;
|
||||||
build_canonical_object_resource(config.bucket(), object_key.trim_start_matches('/'))
|
let object_path = object_key.map(str::trim).filter(|value| !value.is_empty());
|
||||||
}
|
let canonical_uri = build_v4_canonical_uri(config.bucket(), object_path);
|
||||||
None => format!("/{}/", config.bucket()),
|
let body_sha256 = OSS_UNSIGNED_PAYLOAD.to_string();
|
||||||
};
|
let mut signed_headers = BTreeMap::from([
|
||||||
let canonicalized_oss_headers = build_canonicalized_oss_headers(oss_headers);
|
(
|
||||||
let string_to_sign = format!(
|
"host".to_string(),
|
||||||
"{}\n\n{}\n{}\n{}{}",
|
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(),
|
method.as_str(),
|
||||||
content_type.unwrap_or_default(),
|
&canonical_uri,
|
||||||
date,
|
"",
|
||||||
canonicalized_oss_headers,
|
&canonical_headers,
|
||||||
canonical_resource
|
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
|
let mut builder = client
|
||||||
.request(method, target_url)
|
.request(method, target_url)
|
||||||
.header("Date", date)
|
.header("x-oss-content-sha256", body_sha256)
|
||||||
|
.header("x-oss-date", signed_at_text)
|
||||||
.header(
|
.header(
|
||||||
"Authorization",
|
"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 {
|
if let Some(content_type) = content_type {
|
||||||
@@ -1002,13 +1064,160 @@ fn signed_request_builder(
|
|||||||
Ok(builder)
|
Ok(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_canonicalized_oss_headers(headers: &BTreeMap<String, String>) -> String {
|
fn build_v4_signature_scope(endpoint: &str, signed_at: OffsetDateTime) -> Result<String, OssError> {
|
||||||
|
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<String, OssError> {
|
||||||
|
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<String, OssError> {
|
||||||
|
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<String, OssError> {
|
||||||
|
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<Vec<u8>, 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<Vec<u8>, 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::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_v4_canonical_headers(headers: &BTreeMap<String, String>) -> String {
|
||||||
headers
|
headers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| format!("{}:{}\n", key.to_ascii_lowercase(), value.trim()))
|
.map(|(key, value)| format!("{}:{}\n", key.to_ascii_lowercase(), value.trim()))
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_canonical_query_string(params: &BTreeMap<String, String>) -> String {
|
||||||
|
params
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| format!("{}={}", encode_url_query_value(key), encode_url_query_value(value)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("&")
|
||||||
|
}
|
||||||
|
|
||||||
fn encode_url_path(path: &str) -> String {
|
fn encode_url_path(path: &str) -> String {
|
||||||
path.split('/')
|
path.split('/')
|
||||||
.map(encode_url_query_value)
|
.map(encode_url_query_value)
|
||||||
@@ -1115,8 +1324,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(response.bucket, "genarrative-assets".to_string());
|
assert_eq!(response.bucket, "genarrative-assets".to_string());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response.form_fields.oss_access_key_id,
|
response.form_fields.signature_version,
|
||||||
"test-access-key-id".to_string()
|
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!(
|
assert_eq!(
|
||||||
response.form_fields.metadata.get("x-oss-meta-asset-kind"),
|
response.form_fields.metadata.get("x-oss-meta-asset-kind"),
|
||||||
@@ -1169,6 +1390,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy["conditions"][4],
|
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"])
|
json!(["eq", "$content-type", "image/png"])
|
||||||
);
|
);
|
||||||
assert_eq!(response.bucket, "genarrative-assets".to_string());
|
assert_eq!(response.bucket, "genarrative-assets".to_string());
|
||||||
@@ -1206,10 +1439,13 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
response
|
response
|
||||||
.signed_url
|
.signed_url
|
||||||
.contains("OSSAccessKeyId=test-access-key-id")
|
.contains("x-oss-signature-version=OSS4-HMAC-SHA256")
|
||||||
);
|
);
|
||||||
assert!(response.signed_url.contains("&Expires="));
|
assert!(response
|
||||||
assert!(response.signed_url.contains("&Signature="));
|
.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]
|
#[test]
|
||||||
@@ -1282,7 +1518,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonicalized_oss_headers_matches_oss_v1_upload_signature_shape() {
|
fn canonicalized_oss_headers_matches_sorted_v4_header_shape() {
|
||||||
let headers = BTreeMap::from([
|
let headers = BTreeMap::from([
|
||||||
(
|
(
|
||||||
"x-oss-meta-source-job-id".to_string(),
|
"x-oss-meta-source-job-id".to_string(),
|
||||||
@@ -1295,7 +1531,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(
|
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"
|
"x-oss-meta-asset-kind:character_visual\nx-oss-meta-source-job-id:job_001\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
platform-oss = { path = "../platform-oss" }
|
platform-oss = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -525,9 +525,13 @@ pub struct DirectUploadTicketPayload {
|
|||||||
pub struct DirectUploadTicketFormFields {
|
pub struct DirectUploadTicketFormFields {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub policy: String,
|
pub policy: String,
|
||||||
#[serde(rename = "OSSAccessKeyId")]
|
#[serde(rename = "x-oss-signature-version")]
|
||||||
pub oss_access_key_id: String,
|
pub signature_version: String,
|
||||||
#[serde(rename = "Signature")]
|
#[serde(rename = "x-oss-credential")]
|
||||||
|
pub credential: String,
|
||||||
|
#[serde(rename = "x-oss-date")]
|
||||||
|
pub date: String,
|
||||||
|
#[serde(rename = "x-oss-signature")]
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
#[serde(rename = "success_action_status")]
|
#[serde(rename = "success_action_status")]
|
||||||
pub success_action_status: String,
|
pub success_action_status: String,
|
||||||
@@ -615,7 +619,9 @@ impl From<OssPostObjectFormFields> for DirectUploadTicketFormFields {
|
|||||||
Self {
|
Self {
|
||||||
key: value.key,
|
key: value.key,
|
||||||
policy: value.policy,
|
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,
|
signature: value.signature,
|
||||||
success_action_status: value.success_action_status,
|
success_action_status: value.success_action_status,
|
||||||
content_type: value.content_type,
|
content_type: value.content_type,
|
||||||
@@ -703,7 +709,7 @@ mod tests {
|
|||||||
fn direct_upload_ticket_response_keeps_form_fields_shape() {
|
fn direct_upload_ticket_response_keeps_form_fields_shape() {
|
||||||
let payload = serde_json::to_value(CreateDirectUploadTicketResponse {
|
let payload = serde_json::to_value(CreateDirectUploadTicketResponse {
|
||||||
upload: DirectUploadTicketPayload::from(OssPostObjectResponse {
|
upload: DirectUploadTicketPayload::from(OssPostObjectResponse {
|
||||||
signature_version: "v1",
|
signature_version: "v4",
|
||||||
provider: "aliyun-oss",
|
provider: "aliyun-oss",
|
||||||
bucket: "genarrative-assets".to_string(),
|
bucket: "genarrative-assets".to_string(),
|
||||||
endpoint: "oss-cn-shanghai.aliyuncs.com".to_string(),
|
endpoint: "oss-cn-shanghai.aliyuncs.com".to_string(),
|
||||||
@@ -719,7 +725,9 @@ mod tests {
|
|||||||
form_fields: OssPostObjectFormFields {
|
form_fields: OssPostObjectFormFields {
|
||||||
key: "generated-characters/hero/master.png".to_string(),
|
key: "generated-characters/hero/master.png".to_string(),
|
||||||
policy: "policy".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(),
|
signature: "sig".to_string(),
|
||||||
success_action_status: "200".to_string(),
|
success_action_status: "200".to_string(),
|
||||||
content_type: Some("image/png".to_string()),
|
content_type: Some("image/png".to_string()),
|
||||||
@@ -732,10 +740,14 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.expect("payload should serialize");
|
.expect("payload should serialize");
|
||||||
|
|
||||||
assert_eq!(payload["upload"]["signatureVersion"], json!("v1"));
|
assert_eq!(payload["upload"]["signatureVersion"], json!("v4"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["upload"]["formFields"]["OSSAccessKeyId"],
|
payload["upload"]["formFields"]["x-oss-signature-version"],
|
||||||
json!("ak")
|
json!("OSS4-HMAC-SHA256")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["upload"]["formFields"]["x-oss-credential"],
|
||||||
|
json!("ak/20260507/cn-shanghai/oss/aliyun_v4_request")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["upload"]["formFields"]["x-oss-meta-asset-kind"],
|
payload["upload"]["formFields"]["x-oss-meta-asset-kind"],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
time = { workspace = true, features = ["formatting", "parsing"] }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||||
|
|||||||
@@ -5,23 +5,23 @@ version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
module-ai = { path = "../module-ai" }
|
module-ai = { workspace = true }
|
||||||
module-assets = { path = "../module-assets" }
|
module-assets = { workspace = true }
|
||||||
module-big-fish = { path = "../module-big-fish" }
|
module-big-fish = { workspace = true }
|
||||||
module-combat = { path = "../module-combat" }
|
module-combat = { workspace = true }
|
||||||
module-custom-world = { path = "../module-custom-world" }
|
module-custom-world = { workspace = true }
|
||||||
module-inventory = { path = "../module-inventory" }
|
module-inventory = { workspace = true }
|
||||||
module-match3d = { path = "../module-match3d" }
|
module-match3d = { workspace = true }
|
||||||
module-npc = { path = "../module-npc" }
|
module-npc = { workspace = true }
|
||||||
module-puzzle = { path = "../module-puzzle" }
|
module-puzzle = { workspace = true }
|
||||||
module-runtime = { path = "../module-runtime" }
|
module-runtime = { workspace = true }
|
||||||
module-runtime-story = { path = "../module-runtime-story" }
|
module-runtime-story = { workspace = true }
|
||||||
module-runtime-item = { path = "../module-runtime-item" }
|
module-runtime-item = { workspace = true }
|
||||||
module-square-hole = { path = "../module-square-hole" }
|
module-square-hole = { workspace = true }
|
||||||
module-story = { path = "../module-story" }
|
module-story = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
shared-contracts = { path = "../shared-contracts" }
|
shared-contracts = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
shared-kernel = { workspace = true }
|
||||||
spacetimedb-sdk = "2.1.0"
|
spacetimedb-sdk = { workspace = true }
|
||||||
tokio = { version = "1", features = ["rt", "sync", "time"] }
|
tokio = { workspace = true, features = ["rt", "sync", "time"] }
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { workspace = true }
|
||||||
serde_json = "1"
|
serde_json = { workspace = true }
|
||||||
spacetimedb-lib = { version = "=2.1.0", default-features = false, features = ["serde"] }
|
module-ai = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-ai = { path = "../module-ai", default-features = false, features = ["spacetime-types"] }
|
module-assets = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-assets = { path = "../module-assets", default-features = false, features = ["spacetime-types"] }
|
module-big-fish = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-big-fish = { path = "../module-big-fish", default-features = false, features = ["spacetime-types"] }
|
module-combat = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-combat = { path = "../module-combat", default-features = false, features = ["spacetime-types"] }
|
module-inventory = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-inventory = { path = "../module-inventory", default-features = false, features = ["spacetime-types"] }
|
module-custom-world = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-custom-world = { path = "../module-custom-world", default-features = false, features = ["spacetime-types"] }
|
module-match3d = { workspace = true }
|
||||||
module-match3d = { path = "../module-match3d", default-features = false }
|
module-npc = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-npc = { path = "../module-npc", default-features = false, features = ["spacetime-types"] }
|
module-puzzle = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-puzzle = { path = "../module-puzzle", default-features = false, features = ["spacetime-types"] }
|
module-progression = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-progression = { path = "../module-progression", default-features = false, features = ["spacetime-types"] }
|
module-quest = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-quest = { path = "../module-quest", default-features = false, features = ["spacetime-types"] }
|
module-runtime = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-runtime = { path = "../module-runtime", default-features = false, features = ["spacetime-types"] }
|
module-runtime-item = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-runtime-item = { path = "../module-runtime-item", default-features = false, features = ["spacetime-types"] }
|
module-square-hole = { workspace = true }
|
||||||
module-square-hole = { path = "../module-square-hole", default-features = false }
|
module-story = { workspace = true, features = ["spacetime-types"] }
|
||||||
module-story = { path = "../module-story", default-features = false, features = ["spacetime-types"] }
|
shared-kernel = { workspace = true }
|
||||||
shared-kernel = { path = "../shared-kernel" }
|
|
||||||
spacetimedb = { workspace = true, features = ["unstable"] }
|
spacetimedb = { workspace = true, features = ["unstable"] }
|
||||||
|
spacetimedb-lib = { workspace = true, features = ["serde"] }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::runtime::analytics_date_dimension::analytics_date_dimension;
|
use crate::runtime::analytics_date_dimension::analytics_date_dimension;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use spacetimedb_lib::sats::de::serde::DeserializeWrapper;
|
use spacetimedb::sats::de::serde::DeserializeWrapper;
|
||||||
use spacetimedb_lib::sats::ser::serde::SerializeWrapper;
|
use spacetimedb::sats::ser::serde::SerializeWrapper;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::big_fish::big_fish_runtime_run;
|
use crate::big_fish::big_fish_runtime_run;
|
||||||
|
|||||||
Reference in New Issue
Block a user