# 配置同步/服务端 本文说明 `cdxs` 当前配置同步和同步服务端功能的实现方式、调用机制和运行过程。 ## 功能范围 配置同步/服务端主要覆盖以下能力: - 启动一个最小 HTTP 同步服务。 - 在服务端配置文件中添加或更新登录用户。 - 客户端使用用户名和密码登录同步服务。 - 客户端把本地可移植配置推送到服务端。 - 客户端从服务端拉取可移植配置并覆盖本地状态。 - 查看当前客户端同步配置状态。 - 使用 Docker 或 docker-compose 运行同步服务。 当前同步的数据只包含 `cdxs.toml` 中的可移植状态: - `meta` - `accounts` - `homes` 不会同步客户端本地的 `sync` 配置,也不会把服务端的 `server.users`、`server.sessions` 下发给客户端。 ## 核心文件 - `src/main.rs`:CLI 入口,负责把 `server` 和 `sync` 命令分发到具体模块。 - `src/cli.rs`:定义同步服务端和客户端命令参数。 - `src/server.rs`:HTTP 同步服务端实现,包含用户管理、登录、鉴权、状态读写。 - `src/sync_client.rs`:客户端同步逻辑,包含登录、拉取、推送和状态查看。 - `src/config_store.rs`:`cdxs.toml` 数据模型、加载、保存和默认 home 初始化。 - `Dockerfile`:构建并运行 `cdxs server run` 的容器镜像。 - `docker-compose.yml`:使用具名 volume 持久化 `/data/cdxs.toml` 并暴露 `8765` 端口。 ## 数据保存方式 同步功能复用 `Store` 结构,也就是 `cdxs.toml` 的配置模型。 客户端默认读取当前 `CODEX_HOME` 下的配置: ```text \cdxs.toml ``` 如果没有设置 `CODEX_HOME`,默认使用: ```text %USERPROFILE%\.codex\cdxs.toml ``` 服务端有两种数据路径: - 如果 `server run --data <路径>` 被指定,服务端直接使用这个路径保存数据。 - 如果未指定 `--data`,服务端使用当前 `CODEX_HOME` 下的 `cdxs.toml`。 `server user add` 也使用同一套路径规则,因此添加用户和启动服务必须指向同一个数据文件,才能让服务端读取到该用户。 ## Store 中的相关字段 `Store` 中和同步相关的字段包括: - `meta`:当前账号、当前 home 等元数据。 - `accounts`:账号列表。 - `homes`:受管理的 `CODEX_HOME` 列表。 - `server`:服务端用户和 bearer session,仅服务端使用。 - `sync`:客户端保存的服务端地址、用户名、token、最近拉取/推送时间。 服务端用户保存在: ```text Store.server.users ``` 服务端登录 session 保存在: ```text Store.server.sessions ``` 客户端同步配置保存在: ```text Store.sync ``` ## 服务端用户与 Token 原理 添加用户时,`server::add_user` 会生成随机 salt,并把密码保存为哈希值。 密码哈希方式: ```text sha256(salt + ":" + password) ``` 登录成功后,服务端生成一个随机 bearer token,并只保存 token 的哈希值。 token 哈希方式: ```text sha256("cdxs-token:" + token) ``` 原始 token 只在 `/v1/login` 响应中返回一次,由客户端保存到本地 `Store.sync.token`。后续 `pull` 和 `push` 使用: ```http Authorization: Bearer ``` 服务端收到请求后,对请求中的 token 重新计算哈希,并检查是否存在于 `Store.server.sessions`。 ```mermaid flowchart TD A[server user add] --> B[生成 salt] B --> C[计算 password_hash] C --> D[写入 Store.server.users] E[sync login] --> F[校验 username/password] F --> G[生成 bearer token] G --> H[保存 token_hash 到 Store.server.sessions] H --> I[原始 token 返回给客户端] J[sync pull/push] --> K[Authorization Bearer token] K --> L[服务端计算 token_hash] L --> M{sessions 中是否存在} M -->|是| N[允许访问 /v1/state] M -->|否| O[返回 401] ``` ## HTTP API 同步服务端使用 `axum` 提供三个接口: - `GET /health`:健康检查,返回 `ok`。 - `POST /v1/login`:使用用户名和密码登录,返回 bearer token。 - `GET /v1/state`:鉴权后返回服务端保存的可移植状态。 - `PUT /v1/state`:鉴权后用客户端提交的可移植状态替换服务端状态。 `/v1/state` 的读写都会经过 `authorize` 鉴权。 服务端返回给客户端前会调用 `sanitized_for_client`,把以下字段清空: - `server` - `sync` 这样客户端不会拿到服务端用户、密码哈希、已签发 session,也不会拿到服务端自己的同步配置。 ## 命令调用机制 同步相关命令由 `src/cli.rs` 定义,再由 `src/main.rs` 分发。 ```mermaid flowchart TD A[用户执行 cdxs 命令] --> B[Cli::parse] B --> C[src/main.rs match Commands] C --> D{命令类型} D -->|cdxs server run| E[server::run_server] D -->|cdxs server user add| F[server::add_user] D -->|cdxs sync login| G[sync_client::login] D -->|cdxs sync pull| H[sync_client::pull] D -->|cdxs sync push| I[sync_client::push] D -->|cdxs sync status| J[sync_client::status] ``` ## 添加服务端用户运行过程 命令: ```powershell cdxs server user add --password ``` 可选指定服务端数据文件: ```powershell cdxs server user add --password --data ``` 运行过程: 1. 调用 `resolve_data_path` 解析服务端数据文件路径。 2. 调用 `Store::load_from_path` 加载服务端 `cdxs.toml`,文件不存在时生成默认 store。 3. 校验用户名和密码不能为空。 4. 生成随机 salt。 5. 使用 salt 和密码计算 `password_hash`。 6. 如果用户已存在,替换原用户记录。 7. 如果用户不存在,追加到 `Store.server.users`。 8. 调用 `Store::save_to_path` 保存服务端配置。 ```mermaid sequenceDiagram participant U as 用户 participant M as main.rs participant S as server.rs participant C as config_store.rs U->>M: cdxs server user add alice --password *** M->>S: add_user(data, username, password) S->>S: resolve_data_path S->>C: Store::load_from_path C-->>S: Store S->>S: random_token 生成 salt S->>S: hash_secret S->>S: upsert ServerUser S->>C: save_to_path C-->>U: 用户已添加/更新 ``` ## 启动服务端运行过程 命令: ```powershell cdxs server run ``` 默认监听: ```text 127.0.0.1:8765 ``` 可选参数: ```powershell cdxs server run --bind 0.0.0.0:8765 cdxs server run --data ``` 运行过程: 1. 调用 `resolve_data_path` 解析服务端数据文件和默认 home。 2. 解析 `--bind` 为 `SocketAddr`。 3. 创建 `AppState`,保存数据路径、默认 home 和一个异步 `Mutex`。 4. 注册 `/health`、`/v1/login`、`/v1/state` 路由。 5. 绑定 TCP listener。 6. 调用 `axum::serve` 持续处理请求。 服务端在每个会读写配置文件的请求中都会先获取 `AppState.lock`。这个锁用于串行化文件读写,避免并发请求同时读写同一个 `cdxs.toml`。 ```mermaid flowchart TD A[cdxs server run] --> B[resolve_data_path] B --> C[解析 bind 地址] C --> D[创建 AppState] D --> E[注册 axum Router] E --> F[绑定 TcpListener] F --> G[axum::serve] G --> H[处理 /health /v1/login /v1/state] ``` ## 客户端登录运行过程 命令: ```powershell cdxs sync login --server http://127.0.0.1:8765 --user alice --password ``` 运行过程: 1. 客户端规范化 server URL,去掉末尾 `/`。 2. 向 `/v1/login` 发送 JSON 请求。 3. 服务端加载 `cdxs.toml`。 4. 服务端按用户名查找 `Store.server.users`。 5. 服务端校验密码哈希。 6. 登录成功后生成 bearer token。 7. 服务端保存 token 哈希到 `Store.server.sessions`。 8. 客户端解析响应中的原始 token。 9. 客户端加载本地 `cdxs.toml`。 10. 客户端写入 `Store.sync.server_url`、`Store.sync.username`、`Store.sync.token`。 11. 客户端保存本地配置。 ```mermaid sequenceDiagram participant U as 用户 participant C as sync_client.rs participant H as HTTP participant S as server.rs participant FS as cdxs.toml U->>C: cdxs sync login C->>H: POST /v1/login username/password H->>S: login_handler S->>FS: 加载服务端 Store S->>S: 校验 password_hash S->>S: 生成 bearer token 并保存 token_hash S->>FS: 保存服务端 Store S-->>C: token C->>FS: 加载本地 Store C->>FS: 保存 sync.server_url / username / token C-->>U: sync login 成功 ``` ## 拉取配置运行过程 命令: ```powershell cdxs sync pull ``` 运行过程: 1. 加载本地 `cdxs.toml`。 2. 从 `Store.sync` 读取 server URL 和 token。 3. 如果未登录或缺少 token,返回错误。 4. 向 `/v1/state` 发送 `GET` 请求,并附带 `Authorization` header。 5. 服务端校验 bearer token。 6. 服务端加载服务端 `Store`。 7. 服务端清空 `server` 和 `sync` 字段后返回 JSON。 8. 客户端解析远端 `Store`。 9. 客户端用远端 `meta`、`accounts`、`homes` 覆盖本地对应字段。 10. 客户端保留本地 `sync` 配置,并更新 `last_pull_at`。 11. 保存本地 `cdxs.toml`。 ```mermaid flowchart TD A[cdxs sync pull] --> B[加载本地 Store] B --> C[sync_endpoint 读取 server/token] C --> D[GET /v1/state] D --> E[服务端 authorize] E --> F[服务端 sanitized_for_client] F --> G[返回远端 Store] G --> H[覆盖本地 meta/accounts/homes] H --> I[更新 last_pull_at] I --> J[保存本地 cdxs.toml] ``` ## 推送配置运行过程 命令: ```powershell cdxs sync push ``` 运行过程: 1. 加载本地 `cdxs.toml`。 2. 从 `Store.sync` 读取 server URL 和 token。 3. 克隆本地 `Store` 作为上传 payload。 4. 上传前把 payload 中的 `server` 和 `sync` 字段清空。 5. 向 `/v1/state` 发送 `PUT` 请求,并附带 `Authorization` header。 6. 服务端校验 bearer token。 7. 服务端加载原服务端 `Store`。 8. 服务端只用 payload 中的 `meta`、`accounts`、`homes` 替换服务端状态。 9. 服务端把 `sync` 置为默认值,保留原服务端的 `server.users` 和 `server.sessions`。 10. 服务端保存配置。 11. 客户端更新本地 `last_push_at`。 12. 客户端保存本地 `cdxs.toml`。 ```mermaid sequenceDiagram participant C as sync_client.rs participant S as server.rs participant FS as cdxs.toml C->>FS: 加载本地 Store C->>C: payload.server/default, payload.sync/default C->>S: PUT /v1/state + Bearer token S->>FS: 加载服务端 Store S->>S: authorize S->>S: 替换 meta/accounts/homes S->>FS: 保存服务端 Store S-->>C: sanitized Store C->>FS: 更新 last_push_at 并保存 ``` ## 查看同步状态 命令: ```powershell cdxs sync status ``` 输出内容来自本地 `Store.sync`: - `server` - `user` - `token` - `last_pull_at` - `last_push_at` 如果本地保存了 token,输出只显示: ```text ``` 不会打印 token 明文。 ## Docker 运行方式 `Dockerfile` 使用两阶段构建: 1. 使用 `rust:1-bookworm` 构建 release 版本 `cdxs`。 2. 使用 `debian:bookworm-slim` 作为运行镜像。 3. 安装 `ca-certificates`。 4. 把 `cdxs` 复制到 `/usr/local/bin/cdxs`。 5. 暴露 `8765` 端口。 6. 声明 `/data` volume。 容器默认命令: ```text cdxs server run --bind 0.0.0.0:8765 --data /data/cdxs.toml ``` `docker-compose.yml` 会: - 构建当前目录镜像。 - 将容器命名为 `cdxs-server`。 - 设置 `restart: unless-stopped`。 - 映射宿主机 `8765` 到容器 `8765`。 - 使用具名 volume `cdxs-data` 挂载到 `/data`。 ## 写入安全机制 服务端和客户端保存 `cdxs.toml` 都调用 `Store::save` 或 `Store::save_to_path`。 保存时会: 1. 如果目标文件已存在,先备份到对应 home 下的 `cdxs-backups`。 2. 将 `Store` 序列化为 TOML。 3. 写入同目录临时文件。 4. 使用 rename 替换目标文件。 服务端请求还会使用 `tokio::sync::Mutex` 串行化文件读写,降低并发请求导致状态覆盖或文件损坏的风险。 ## 当前实现边界 - 同步没有字段级合并或冲突解决,`pull` 会用远端 `meta/accounts/homes` 覆盖本地,`push` 会用本地 `meta/accounts/homes` 覆盖服务端。 - bearer session 当前没有过期时间和撤销命令。 - 服务端用户只有添加/更新命令,没有删除、列表或改密命令。 - 服务端没有多租户隔离;所有通过鉴权的用户访问同一份服务端状态。 - HTTP 服务没有在代码中直接配置 TLS,需要由外部反向代理或部署环境处理。 - `sync push` 不上传客户端本地 `sync` token,也不上传客户端本地 `server` 配置。 - `sync pull` 不会写入远端 `server` 或远端 `sync` 字段,只更新本地可移植状态并保留本地同步登录信息。