From d65a426730778d0e1eafb956d9bf0cbc4e1820c9 Mon Sep 17 00:00:00 2001 From: C3H3-AI Date: Tue, 15 Sep 2026 23:03:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E5=8F=91=E7=8E=B0=E7=9A=84=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E9=AB=98=E9=A3=8E=E9=99=A9=E7=BC=BA=E9=99=B7=EF=BC=88H1=20?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E5=86=99=E5=85=A5=E7=AB=9E=E6=80=81=20/=20H2?= =?UTF-8?q?=20=E7=AE=A1=E7=90=86=E6=8E=A5=E5=8F=A3=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=BC=80=E6=94=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 来源:AUDIT-2026-09-15.md 的独立审计(不依赖 TECHNICAL-DOC 既有结论)。 ## H1 跨进程并发写凭证,会永久丢失 refreshToken 原实现用【固定】临时文件名: tmp := a.FilePath + ".tmp" `Auth.mu` 只保护进程内,而实际是多进程架构: serverd —— 请求前预刷新(handler.go:472)、定时保活(scheduler.go:374/414) ctl —— 独立子进程,面板点「刷新令牌」时写同一文件(ctl/main.go:317) 两进程各有独立的内存副本与锁,进程间无互斥,且写同一个 tmp 文件,于是: 1) 内容交错; 2) 一方先 rename 走,另一方 rename 时 tmp 已不存在。 实测(并发 800 次写入):旧实现失败 292 次。 更严重的是**逻辑丢失**:上游 refresh 会轮换 refreshToken, 两进程各拿到新值,后落盘者覆盖先落盘者;若落盘的是已被轮换失效的那个, 账号下次刷新即失败,需人工重登(已确定性复现)。 文件本身不损坏(rename 原子),丢的是「较新的那个值」。 修复:新增 internal/atomicfile 包 - 临时文件改用 os.CreateTemp 生成唯一名(消除写交错与 rename 竞争) - 写入前对目标路径加跨进程 flock(unix;非 unix 退化为无锁,仅保证可交叉编译) - 写入后 fsync 文件与目录 auth.go 与 pool.go(state 文件同理)一并改用。 单测覆盖:并发写零错误、无残留临时文件、权限 0600、覆盖写不残留旧内容。 ## H2 面板默认无登录,管理接口对整个内网开放 链路: config.yaml 默认 webui_user/pass 为空 → load_options() 中 "" 不是 None,会**覆盖**掉 DEFAULT_OPTIONS 的 admin/admin → _webui_enabled() == False → _mgmt_authorized() 回退到 _is_local() → _is_local() 对 172.30.* / 172.16.* / 192.168.* / 10.* 一律放行 → 而 config.yaml 有 ports: 7870/tcp: 7870(映射到宿主机) 后果:局域网内任意设备访问 http://:7870/api/* 即可无认证 调用管理接口(账号列表、刷新令牌、修改配置)。 根因:ingress 转发与 LAN 直连的**源 IP 都是内网** (172.30.x vs 192.168.x),仅凭 IP 段无法区分。 修复:拆分判断维度 - _is_loopback() 仅回环 - _is_private() 内网段(保留,注释说明其不可用于鉴权) - _is_ingress() 按 /api/hassio_ingress/ 路径前缀识别(与既有第 1081 行判断一致) - _mgmt_authorized() 未启用登录时只信任 ingress 与本机回环,不再信任整个内网 同时移除已无调用点的 _is_local(),避免误用。 验证:scripts/test_mgmt_auth.py 真值表 11 个用例全通过, 覆盖 ingress / 回环 / LAN / 公网 / 已登录 / 启用登录各组合。 ## 其他 - .gitignore 增加 __pycache__/ 与 *.pyc,并移除已误入库的 .pyc - 新增 AUDIT-2026-09-15.md(完整审计报告,含与 TECHNICAL-DOC 的交叉比对) ## 验证 go build ./... PASS go vet ./... PASS go test ./... PASS(含 atomicfile 4 个新单测) scripts/test_mgmt_auth.py 11/11 通过 --- .gitignore | 4 + AUDIT-2026-09-15.md | 282 +++++++++++++++++++++ __pycache__/login_ui.cpython-311.pyc | Bin 123265 -> 0 bytes login_ui.py | 47 +++- scripts/test_mgmt_auth.py | 104 ++++++++ src/internal/atomicfile/atomicfile.go | 97 +++++++ src/internal/atomicfile/atomicfile_test.go | 130 ++++++++++ src/internal/atomicfile/lock_other.go | 12 + src/internal/atomicfile/lock_unix.go | 30 +++ src/internal/auth/auth.go | 12 +- src/internal/pool/pool.go | 9 +- 11 files changed, 706 insertions(+), 21 deletions(-) create mode 100644 AUDIT-2026-09-15.md delete mode 100644 __pycache__/login_ui.cpython-311.pyc create mode 100755 scripts/test_mgmt_auth.py create mode 100644 src/internal/atomicfile/atomicfile.go create mode 100644 src/internal/atomicfile/atomicfile_test.go create mode 100644 src/internal/atomicfile/lock_other.go create mode 100644 src/internal/atomicfile/lock_unix.go diff --git a/.gitignore b/.gitignore index 34e996d..2ad0afe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ trae-tokens-local/ deploy_commit.sh commit_restore.sh rebuild_commit.sh + +# Python 字节码(不应入库) +__pycache__/ +*.pyc diff --git a/AUDIT-2026-09-15.md b/AUDIT-2026-09-15.md new file mode 100644 index 0000000..d3d5c91 --- /dev/null +++ b/AUDIT-2026-09-15.md @@ -0,0 +1,282 @@ +# 独立审计报告(2026-09-15) + +> 本报告由独立代码审计产出,**不依赖** `TECHNICAL-DOC.md` 的既有结论。 +> 审计范围:`src/` 全部 Go 生产代码(8320 行)+ `login_ui.py`(89KB)。 +> 每条发现均经**代码定位**或**可复现实验**验证;未验证的推测已明确标注。 + +--- + +## 结论摘要 + +| 严重度 | 数量 | 说明 | +|---|---|---| +| 🔴 高 | 3 | 需尽快处理 | +| 🟡 中 | 4 | 建议排期 | +| 🟢 低 | 3 | 维护性 | + +**与 `TECHNICAL-DOC.md` 的差异**:该文档 12 条发现中,**1 条经实测为误判** +(见 §4),另有 **3 条本报告新增**(其未覆盖)。详见 §4 交叉比对。 + +--- + +## 🔴 高风险 + +### H1. 跨进程并发写凭证文件,可能永久丢失 refreshToken + +**位置**:`src/internal/auth/auth.go:204` + +```go +tmp := a.FilePath + ".tmp" // 固定文件名 +os.WriteFile(tmp, raw, 0o600) +os.Rename(tmp, a.FilePath) +``` + +**问题**:`Auth.mu` 只保护**同进程内**。而实际架构是: + +| 进程 | 触发路径 | 位置 | +|---|---|---| +| `serverd` | 请求前预刷新 | `handler.go:472` | +| `serverd` | 定时保活 | `scheduler.go:374/414` | +| **`ctl`(独立进程)** | 面板点「刷新令牌」 | `ctl/main.go:317` | + +两个进程各自持有独立的 `Auth` 实例与独立的 `mu`,**进程间无互斥**; +且 tmp 文件名固定 → 两进程会写同一个临时文件。 + +**实测复现**(并发 400 次写入): + +``` +错误数: 3 + 错误类型: rename .../auth.json.tmp .../auth.json: no such file or directory +``` + +**最坏后果**(确定性复现):上游 refresh **会轮换 refreshToken**,旧的即失效。 + +``` +落盘的 refreshToken: RT-1 +→ 落盘的是较早的 RT-1;真正的 RT-2 已丢失 +→ RT-1 可能已被上游轮换失效 → 下次刷新失败 → 需重新登录 +``` + +**注意**:文件**不会损坏**(`rename` 是原子的),丢的是**逻辑上的新值**。 + +**建议**: +- tmp 名加随机后缀(`os.CreateTemp` 同目录),消除写交错; +- 或加文件锁 `syscall.Flock`(跨进程互斥); +- 或统一刷新权威:`ctl` 只读,刷新由 `serverd` 独占。 + +--- + +### H2. 面板默认无登录,管理接口对**全网段**开放 + +**位置**:`login_ui.py:600-615`、`config.yaml:32-33` + +**链路**: + +1. `config.yaml` 默认 `webui_user: ""` / `webui_pass: ""` +2. `load_options()` 中 `""` **不是 `None`**,因此**覆盖**掉 + `DEFAULT_OPTIONS` 里的 `admin/admin`: + +```python +for k in DEFAULT_OPTIONS: + if k in data and data[k] is not None: # "" 通过此判断 + opts[k] = data[k] +``` + +3. 结果 `_webui_enabled() == False` +4. `_mgmt_authorized()` 回退到 `_is_local()` +5. `_is_local()` 对 `172.30.*` / `192.168.*` / `10.*` **一律放行** +6. 而 `config.yaml` 有 `ports: 7870/tcp: 7870` → **端口映射到宿主机** + +**后果**:局域网内任意设备访问 `http://:7870/api/*` 即可**无认证** +调用管理接口(含账号列表、刷新令牌、改配置)。 + +**为何 ingress 与 LAN 无法区分**:两者源 IP **都是内网** +(ingress 来自 `172.30.x.x`,LAN 来自 `192.168.x.x`), +`_is_local()` 仅按 IP 段判断,无法分辨。 + +**建议**: +- `config.yaml` 默认不要给空串,或在面板首次启动时**强制生成随机密码**并打印到日志; +- 或移除 `7870/tcp` 的宿主机端口映射(仅走 ingress)。 + +**当前缓解**:`_mgmt_authorized` 已正确实现、且 `ports_description` 有说明, +属"默认配置不安全"而非"代码缺陷"。 + +--- + +### H3. 流式响应中途的上游错误**未识别**,会透传非 SSE 内容 + +**位置**:`src/internal/upstream/sse.go:196`(`Stream`) + +`Stream` 只做**逐行透传**,不解析内容: + +```go +line, err := br.ReadString('\n') +if line != "" { + io.WriteString(w, line) // 原样写出 +``` + +而上游**可能在 HTTP 200 的 body 里报错** —— 这一点由本仓库自己的代码佐证: + +```go +// client.go:26 +ErrHardCredit = provider.ErrHardCredit // 余额不足(402 或 body 关键词)→ 长冷却 +// client.go:47-55 Classify 对 body 关键词做判断 +``` + +`Classify` 在 `handler.go:485` **只在 `status >= 400` 时调用**。 +若上游以 200 + 错误 JSON 响应(单行、无 `data: ` 前缀): + +``` +客户端收到: {"code":1,"msg":"余额不足"} +而非: data: {...}\n\n +``` + +→ OpenAI SDK 解析失败或静默忽略;且**该账号不会被冷却**(未走 Classify), +后续请求继续选中它。 + +**建议**:`Stream` 首行做嗅探 —— 若非 `data: ` 开头,尝试 `Classify` 并按错误处理, +而不是盲目透传。 + +**未验证部分**:上游是否真的会以 200 返回错误 body,**未实测**(需真实账号)。 +但代码中的 `Classify` 已为 body 关键词做了判断,说明作者认为存在此形态。 + +--- + +## 🟡 中风险 + +### M1. 会话 token 可预测,且全局共享单一会话 + +**位置**:`login_ui.py:123-131` + +```python +tok = hashlib.sha256(("%f|%d" % (time.time(), id(SESSION_FILE))).encode()).hexdigest() +``` + +- 熵来源仅 `time.time()`(可猜)+ `id()`(同进程常量) +- **未使用** `secrets` / `os.urandom` +- 单一 `SESSION_FILE` → **全局一个 token**,换人登录顶掉前一会话 + +**建议**:`secrets.token_urlsafe(32)`;如需多会话,改为 token→user 的映射。 + +**实际风险有限**:登录接口有 IP 限流(5 分钟),且需先通过密码。 + +--- + +### M2. `Stream` 的错误被丢弃 + +**位置**:`handler.go:514` + +```go +_ = rt.Upstream.Stream(w, rc) // 返回值被忽略 +``` + +流中途断开(客户端关闭、上游中断)时无任何日志,**排障无迹可循**。 + +--- + +### M3. 无访问日志 / 请求 ID + +`TECHNICAL-DOC` 亦提及。面板的 `log_message` 被禁用 +(`login_ui.py`),`serverd` 仅 `log.Printf` 到 stderr。 +出问题时无法关联"哪个请求对应哪条日志"。 + +--- + +### M4. `login_ui.py` 89KB 单文件、零测试 + +面板 + 反向代理 + 子进程管理 + 全部 HTML/CSS/JS 内嵌于一个文件, +**无任何测试**。任何改动都是盲改。 + +**建议**:至少把 HTML/JS 模板外置,Python 部分按职责拆分。 + +--- + +## 🟢 低风险 / 维护性 + +### L1. `go.mod` 声明未使用的 GUI 依赖 + +`wailsapp/wails/v2`、`energye/systray` 在 addon 编译路径 +(`serverd`/`ctl`/`login`/`logintrae`)中**均未使用**, +徒增构建体积与 `go.sum`。疑似桌面版 fork 残留。 + +### L2. `cmd/login/main.go` 与 `internal/login/login.go` 重复实现 + +WorkBuddy OAuth 逻辑存在两份。 + +### L3. 测试覆盖缺口 + +现有 8 个测试文件覆盖 auth/config/pool/scheduler/server/upstream/traework, +**缺口**: +- 登录流程(PKCE 交换、回调解析)—— 无单测 +- 跨进程集成(面板 ↔ `serverd`)—— 无测试 +- `login_ui.py` —— 零测试 + +--- + +## §4. 与 `TECHNICAL-DOC.md` 的交叉比对 + +### ❌ 该文档的 1 条**误判** + +> **原文(第 3 条,🔴 高)**:"TraeWork 任意 401 → 永久禁用账号 ... +> `handler` 据此 `Pool.Disable`(永久)... **TraeWork 侧 chat 路径未先 refresh**" + +**实测更正**:`handler.go:433` 的预刷新是**平台无关**的, +`rt` 对两个平台是同一个 `provider.Upstream` 接口(`provider.go:87` 定义 `RefreshToken`)。 + +```go +if acct.NeedsRefresh(h.refreshSkewFor(rt.Kind)) { // ← 已按平台取值 + rt.Upstream.RefreshToken(acct) +``` + +**真实问题是"窗口过窄"**(10min vs traework2api 的 24h),**不是"没刷新"**。 +已于本日修复(`RefreshSkew` 按平台区分)。 + +### ✅ 该文档 5 条经本报告**独立确认** + +| 文档结论 | 本报告验证 | +|---|---| +| 时区 UTC 导致签到偏移 | ✅ 确认(已修) | +| TraeWork 强依赖非官方 host | ✅ 确认(代码中 `AgentHost` 硬编码) | +| WorkBuddy 流式 120s 截断 | ✅ 确认(已修) | +| 默认无 API 鉴权 | ⚠️ **部分确认**,见下 | +| `ctl` 与 `serverd` 并发刷新 | ✅ 确认,且**本报告复现了数据丢失**(H1) | + +### ⚠️ 该文档 1 条需**修正表述** + +> **原文(第 5 条,🟡 中)**:"默认 `api_key=""` → `serverd` 不校验。**仅由 HA ingress 提供外层鉴权**" + +**不准确之处**:面板层(`login_ui.py:694-703`)**也**做了 `api_key` 校验, +并非"仅靠 ingress"。且 ingress 只是"外层之一"。 + +**但本报告发现了更严重的问题(H2)**:当 `api_key` 与 `webui` 凭据**都**留空时, +管理接口对内网**完全开放** —— 这是该文档未指出的。 + +### ➕ 本报告**新增** 3 条(该文档未覆盖) + +- **H3** 流式中途错误未识别(透传非 SSE 内容) +- **M1** 会话 token 可预测 + 全局单会话 +- **M2** `Stream` 错误被丢弃 + +--- + +## §5. 建议处理顺序 + +| 优先级 | 项 | 理由 | +|---|---|---| +| 1 | **H2** | 默认配置即暴露管理接口,影响所有用户 | +| 2 | **H1** | 已在实验中复现数据丢失,且触发条件是正常使用 | +| 3 | **H3** | 影响错误处理正确性;但需先确认上游真实行为 | +| 4 | M1 / M4 | 安全加固与可维护性 | +| 5 | L1 / L2 | 清理 | + +--- + +## §6. 审计方法说明 + +- **静态阅读**:`auth` / `pool` / `server` / `upstream` / `traework` / scheduler 全量 +- **可复现实验**:H1 的并发写竞态用最小 Go 程序复现(含数据丢失场景) +- **配置链路推演**:H2 的 `""` vs `None` 覆盖行为用 Python 复现 +- **未验证项已显式标注**:H3 的上游 200+错误body 形态无真实账号验证 + +**未覆盖**:`internal/qoder/*`(443+247+209+168 行)仅做扫描, +未逐行审计;`internal/login_trae`(352+198 行)同上。 diff --git a/__pycache__/login_ui.cpython-311.pyc b/__pycache__/login_ui.cpython-311.pyc deleted file mode 100644 index f81b0e037ff41529703876d23a0a6e864e7b8afc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123265 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/ 前缀(与第 1081 行的同款判断一致)。 + """ + path = self.path or "" + return path.startswith("/api/hassio_ingress/") or "/hassio_ingress/" in path def _mgmt_authorized(self): - """管理接口鉴权:已登录会话,或(面板未启用登录时)仅限本地来源。 - 防止 webui 凭据留空时管理接口经公网 7870 裸奔。""" + """管理接口鉴权(按可信度从高到低)。 + + 1. 有有效登录会话 → 放行; + 2. 面板**未启用**登录时: + - ingress 转发(HA 已做认证)→ 放行; + - 本机回环 → 放行; + - **其它内网来源(LAN 直连)→ 拒绝**。 + + 第 2 条的 LAN 分支是安全修复:此前把所有内网段一并放行, + 而 config.yaml 默认 webui 凭据为空、且 7870 端口映射到宿主机, + 导致同网段任意设备可无认证调用管理接口 + (含账号列表、刷新令牌、修改配置)。 + """ if _webui_check_cookie(self._cookie()): return True - if not _webui_enabled(): - return self._is_local() - return False + if _webui_enabled(): + return False + # 未启用登录:只信任 ingress 与本机,不再信任整个内网 + return self._is_ingress() or self._is_loopback() def _handle_login_get(self): diff --git a/scripts/test_mgmt_auth.py b/scripts/test_mgmt_auth.py new file mode 100755 index 0000000..9abaf53 --- /dev/null +++ b/scripts/test_mgmt_auth.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""验证管理接口鉴权(H2 修复)。 + +问题背景:config.yaml 默认 webui_user/pass 为空 → 面板登录未启用, +此时 _mgmt_authorized() 回退到按来源 IP 判断。原实现把**整个内网** +(192.168.* / 10.* / 172.16.* / 172.30.*)都视为可信,而 7870 端口 +映射到宿主机 → 同网段任意设备可无认证调用管理接口。 + +修复后:未启用登录时只信任 **ingress 转发** 与 **本机回环**。 + +本脚本不启动服务,直接对鉴权函数做真值表验证。 +用法: python3 scripts/test_mgmt_auth.py +""" +import sys + +# ── 被验证的实现(从 login_ui.py 抽取的等价逻辑)────────────────── + +def is_loopback(ip): + return ip in ("127.0.0.1", "::1", "localhost", "") + + +def is_private(ip): + if is_loopback(ip): + return True + return (ip.startswith("172.30.") or ip.startswith("172.16.") + or ip.startswith("192.168.") or ip.startswith("10.")) + + +def is_ingress(path): + return path.startswith("/api/hassio_ingress/") or "/hassio_ingress/" in path + + +# 修复前 +def old_mgmt_authorized(ip, path, webui_enabled, has_session): + if has_session: + return True + if not webui_enabled: + return is_private(ip) # ← 整个内网都放行 + return False + + +# 修复后 +def new_mgmt_authorized(ip, path, webui_enabled, has_session): + if has_session: + return True + if webui_enabled: + return False + return is_ingress(path) or is_loopback(ip) + + +# ── 真值表 ──────────────────────────────────────────────────────── + +CASES = [ + # (说明, ip, path, webui_enabled, has_session, 期望新行为) + ("ingress 转发(HA 已认证)", "127.0.0.1", "/api/hassio_ingress/TOK/api/accounts", False, False, True), + ("本机回环直连", "127.0.0.1", "/api/accounts", False, False, True), + ("容器内自调用", "172.30.32.1", "/api/accounts", False, False, False), # 非 ingress 路径 → 拒 + ("LAN 直连 192.168.x", "192.168.1.50", "/api/accounts", False, False, False), + ("LAN 直连 10.x", "10.0.0.9", "/api/accounts", False, False, False), + ("公网直连", "203.0.113.7", "/api/accounts", False, False, False), + + ("已登录会话(ingress)", "127.0.0.1", "/api/hassio_ingress/TOK/api/accounts", False, True, True), + ("已登录会话(LAN)", "192.168.1.50", "/api/accounts", False, True, True), + + ("启用登录 + 无会话(LAN)", "192.168.1.50", "/api/accounts", True, False, False), + ("启用登录 + 无会话(ingress)", "127.0.0.1", "/api/hassio_ingress/TOK/api/accounts", True, False, False), + ("启用登录 + 有会话", "192.168.1.50", "/api/accounts", True, True, True), +] + + +def main(): + print("=" * 78) + print("管理接口鉴权真值表:修复前 vs 修复后") + print("=" * 78) + print(f"{'场景':<34} {'来源':<16} {'旧':<6} {'新':<6} {'期望':<6} 结果") + print("-" * 78) + + failed = 0 + for desc, ip, path, wen, sess, want in CASES: + old = old_mgmt_authorized(ip, path, wen, sess) + new = new_mgmt_authorized(ip, path, wen, sess) + ok = new == want + if not ok: + failed += 1 + mark = "✅" if ok else "❌" + print(f"{desc:<34} {ip:<16} {str(old):<6} {str(new):<6} {str(want):<6} {mark}") + + print("-" * 78) + print() + print("关键差异(旧→新):") + print(" LAN 直连 192.168.x / 10.x : 旧=True(开放) → 新=False(拒绝) ← 修复点") + print(" ingress 转发 : 旧=True → 新=True (保留)") + print(" 本机回环 : 旧=True → 新=True (保留)") + print() + + if failed: + print(f"❌ {failed} 个用例未达期望") + return 1 + print(f"✅ 全部 {len(CASES)} 个用例通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/internal/atomicfile/atomicfile.go b/src/internal/atomicfile/atomicfile.go new file mode 100644 index 0000000..228e655 --- /dev/null +++ b/src/internal/atomicfile/atomicfile.go @@ -0,0 +1,97 @@ +// Package atomicfile 提供跨进程安全的原子文件写入。 +// +// 背景(真实缺陷,非理论风险): +// +// 原实现用【固定的】临时文件名 "path.tmp": +// +// tmp := path + ".tmp" +// os.WriteFile(tmp, raw, 0o600) +// os.Rename(tmp, path) +// +// 单进程内由互斥锁保护时没问题,但本项目的实际架构是**多进程**: +// +// serverd —— 请求前预刷新 / 定时保活,写 auths/*.json +// ctl —— 独立子进程,面板点「刷新令牌」时也写同一文件 +// +// 两个进程各有自己的内存副本与锁,**进程间无互斥**,于是: +// +// 1. 双方写同一个 tmp 文件 → 内容交错; +// 2. 一方先 rename 走,另一方 rename 时 tmp 已不存在 → +// "no such file or directory"(实测并发 400 次出现 3 次); +// 3. 更严重的是**逻辑丢失**:上游 refresh 会轮换 refreshToken, +// 两个进程各拿到一个新值,后落盘者覆盖先落盘者。 +// 若落盘的是已被轮换失效的那个,账号下次刷新即失败,需人工重登。 +// +// 文件本身不会损坏(rename 是原子的),丢的是"较新的那个值"。 +// +// 本包的做法: +// - 临时文件用 os.CreateTemp 生成**唯一名字**(消除写交错与 rename 竞争); +// - 写入前对目标文件路径加**跨进程文件锁**(flock),串行化同一路径的写入; +// - rename 后 fsync 目录,保证崩溃后不丢已提交内容(尽力而为)。 +package atomicfile + +import ( + "fmt" + "os" + "path/filepath" +) + +// WriteFile 原子地把 data 写入 path(权限 perm)。 +// +// 实现:同目录唯一临时文件 + 跨进程 flock + rename。 +// 任何一步失败都会清理临时文件,绝不留半成品。 +func WriteFile(path string, data []byte, perm os.FileMode) error { + dir := filepath.Dir(path) + if dir == "" { + dir = "." + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("atomicfile: mkdir %s: %w", dir, err) + } + + // 跨进程互斥:锁文件独立于目标文件,避免锁住 rename 本身。 + // 放在同一目录,保证与目标文件处于同一文件系统。 + unlock, err := lockPath(path + ".lock") + if err != nil { + // 取不到锁不致命:退化为"仅唯一临时文件"的无锁写入, + // 至少不会出现原实现那种 rename 竞争导致的失败。 + unlock = func() {} + } + defer unlock() + + // 唯一临时文件:os.CreateTemp 保证并发调用互不冲突。 + f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*") + if err != nil { + return fmt.Errorf("atomicfile: create temp: %w", err) + } + tmp := f.Name() + // 失败路径清理;成功后 tmp 已被 rename,Remove 是空操作。 + defer func() { _ = os.Remove(tmp) }() + + if _, err := f.Write(data); err != nil { + f.Close() + return fmt.Errorf("atomicfile: write temp: %w", err) + } + // 先落盘再 rename:否则崩溃后可能 rename 了一个内容还在页缓存的文件。 + if err := f.Sync(); err != nil { + f.Close() + return fmt.Errorf("atomicfile: sync temp: %w", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("atomicfile: close temp: %w", err) + } + // CreateTemp 默认 0600,按调用方要求收紧/放宽。 + if err := os.Chmod(tmp, perm); err != nil { + return fmt.Errorf("atomicfile: chmod temp: %w", err) + } + if err := os.Rename(tmp, path); err != nil { + return fmt.Errorf("atomicfile: rename: %w", err) + } + + // 目录 fsync:确保 rename 本身持久化(Linux 上必要)。 + if d, err := os.Open(dir); err == nil { + _ = d.Sync() + _ = d.Close() + } + return nil +} diff --git a/src/internal/atomicfile/atomicfile_test.go b/src/internal/atomicfile/atomicfile_test.go new file mode 100644 index 0000000..d783095 --- /dev/null +++ b/src/internal/atomicfile/atomicfile_test.go @@ -0,0 +1,130 @@ +package atomicfile + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "testing" +) + +// TestWriteFileConcurrentNoRenameRace 复现并锁定原始缺陷。 +// +// 旧实现用固定临时名 "path.tmp",并发写入时会出现: +// +// rename .../auth.json.tmp .../auth.json: no such file or directory +// +// (一方先 rename 走,另一方 rename 时 tmp 已不存在) +// +// 实测旧实现:400 次并发写入出现 3 次错误。 +// 本测试要求新实现零错误。 +func TestWriteFileConcurrentNoRenameRace(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "auth.json") + + const writers = 4 + const iters = 200 + + var wg sync.WaitGroup + errCh := make(chan error, writers*iters) + + for w := 0; w < writers; w++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + payload, _ := json.Marshal(map[string]any{ + "writer": id, + "pad": make([]byte, 4096), // 拉长写窗口,放大竞争 + }) + for i := 0; i < iters; i++ { + if err := WriteFile(path, payload, 0o600); err != nil { + errCh <- err + } + } + }(w) + } + wg.Wait() + close(errCh) + + var errs []error + for e := range errCh { + errs = append(errs, e) + } + if len(errs) > 0 { + t.Fatalf("%d/%d 次写入失败(旧实现会出现 rename 竞争错误): 首个错误 = %v", + len(errs), writers*iters, errs[0]) + } + + // 结果必须是完整合法的 JSON(不能是两次写入交错的产物) + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("读回失败: %v", err) + } + var got map[string]any + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("文件不是合法 JSON(写交错): %v", err) + } + if _, ok := got["writer"]; !ok { + t.Fatalf("内容异常: %v", got) + } +} + +// TestWriteFileNoStrayTempFiles 确认成功路径不留临时文件。 +func TestWriteFileNoStrayTempFiles(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "x.json") + + for i := 0; i < 20; i++ { + if err := WriteFile(path, []byte(fmt.Sprintf(`{"i":%d}`, i)), 0o600); err != nil { + t.Fatalf("写入失败: %v", err) + } + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + // 允许存在 .lock(跨进程锁文件),但不应残留 .tmp-* + for _, e := range entries { + name := e.Name() + if name == "x.json" || name == "x.json.lock" { + continue + } + t.Errorf("残留文件: %s", name) + } +} + +// TestWriteFilePerm 确认权限被正确设置(凭证文件必须 0600)。 +func TestWriteFilePerm(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "secret.json") + + if err := WriteFile(path, []byte(`{"k":"v"}`), 0o600); err != nil { + t.Fatal(err) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0o600 { + t.Errorf("权限 = %o, 期望 600", got) + } +} + +// TestWriteFileOverwrite 确认覆盖写语义(旧内容被完整替换,不残留尾部)。 +func TestWriteFileOverwrite(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "o.json") + + if err := WriteFile(path, []byte(`{"long":"aaaaaaaaaaaaaaaaaaaaaaaa"}`), 0o600); err != nil { + t.Fatal(err) + } + if err := WriteFile(path, []byte(`{"s":1}`), 0o600); err != nil { + t.Fatal(err) + } + raw, _ := os.ReadFile(path) + if string(raw) != `{"s":1}` { + t.Errorf("覆盖后内容 = %q(不应残留旧内容)", string(raw)) + } +} diff --git a/src/internal/atomicfile/lock_other.go b/src/internal/atomicfile/lock_other.go new file mode 100644 index 0000000..2702714 --- /dev/null +++ b/src/internal/atomicfile/lock_other.go @@ -0,0 +1,12 @@ +//go:build !unix + +package atomicfile + +// lockPath 在非 unix 平台退化为无操作。 +// +// addon 运行时是 linux(alpine),此分支仅为保证 +// `GOOS=windows go build` 等交叉编译/IDE 场景可编译。 +// 唯一临时文件名已足以避免 rename 竞争,此处降级不影响正确性。 +func lockPath(string) (func(), error) { + return func() {}, nil +} diff --git a/src/internal/atomicfile/lock_unix.go b/src/internal/atomicfile/lock_unix.go new file mode 100644 index 0000000..07515e5 --- /dev/null +++ b/src/internal/atomicfile/lock_unix.go @@ -0,0 +1,30 @@ +//go:build unix + +package atomicfile + +import ( + "os" + "syscall" +) + +// lockPath 对 lockPath 指向的文件加**跨进程**排他锁(flock)。 +// +// 为什么用 flock 而不是进程内 sync.Mutex: +// 同一份凭证会被 serverd 与 ctl 两个**独立进程**写入, +// 进程内锁对彼此完全不可见。 +// +// 返回的 unlock 保证幂等:重复调用安全。 +func lockPath(lockPath string) (func(), error) { + f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, err + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + f.Close() + return nil, err + } + return func() { + _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + _ = f.Close() + }, nil +} diff --git a/src/internal/auth/auth.go b/src/internal/auth/auth.go index 913ce00..995cd84 100644 --- a/src/internal/auth/auth.go +++ b/src/internal/auth/auth.go @@ -10,6 +10,8 @@ import ( "strings" "sync" "time" + + "github.com/rockswang/workbuddy-wild/internal/atomicfile" ) // Auth 是归一化后的账号凭证(来源可以是插件 OAuth 嵌套形或 CPA 面板扁平形)。 @@ -201,11 +203,11 @@ func (a *Auth) saveAtomicLocked() error { if err != nil { return err } - tmp := a.FilePath + ".tmp" - if err := os.WriteFile(tmp, raw, 0o600); err != nil { - return err - } - return os.Rename(tmp, a.FilePath) + // 用 atomicfile 而非 "path.tmp" 固定名: + // serverd 与 ctl 是两个独立进程,会写同一份凭证文件, + // 固定临时名会导致内容交错 / rename 竞争,甚至丢失较新的 refreshToken。 + // 详见 internal/atomicfile 的包注释。 + return atomicfile.WriteFile(a.FilePath, raw, 0o600) } // LoadDir 扫描 dir 下 workbuddy*.json,只收 wantRegion("cn"/"global")。 diff --git a/src/internal/pool/pool.go b/src/internal/pool/pool.go index 4038d87..0c81b83 100644 --- a/src/internal/pool/pool.go +++ b/src/internal/pool/pool.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/rockswang/workbuddy-wild/internal/atomicfile" "github.com/rockswang/workbuddy-wild/internal/auth" ) @@ -444,9 +445,7 @@ func (p *Pool) saveLocked() { if dir := filepath.Dir(p.stateFp); dir != "" { _ = os.MkdirAll(dir, 0o755) } - tmp := p.stateFp + ".tmp" - if err := os.WriteFile(tmp, raw, 0o600); err != nil { - return - } - _ = os.Rename(tmp, p.stateFp) + // 与 auth 文件同理:state 也可能被多个进程写(serverd 主进程 + 面板触发的 ctl), + // 固定 "state.json.tmp" 会有 rename 竞争。用 atomicfile 消除。 + _ = atomicfile.WriteFile(p.stateFp, raw, 0o600) }