Skip to content

fix(waf): WAF 恢复 Flight 与 UI Host 生命周期解耦 - #1

Open
Cn-Asukai wants to merge 1 commit into
LittleSurvival:mainfrom
Cn-Asukai:waf-flight-lifecycle-rework
Open

Cn-Asukai wants to merge 1 commit into
LittleSurvival:mainfrom
Cn-Asukai:waf-flight-lifecycle-rework

Conversation

@Cn-Asukai

Copy link
Copy Markdown

摘要

将 WAF 挑战恢复从「绑定 UI Host 的一次性尝试」重构为「由 Client 持有、可跨 UI 生命周期存活的长期会话」。Host 挂载/卸载/前后台切换不再杀死恢复流程,而是销毁当前浏览器尝试、暂停挑战预算,等 Host 回来后再开新尝试继续。同时引入 Host 注册身份与 generation 机制,彻底隔离旧实例的迟到事件。

背景与动机

旧实现的 WafChallengeCoordinator 只有两个全局布尔量(hostMounted / foreground),且一旦失去前台立即 failActive(FOREGROUND_REQUIRED),导致:

  1. 旋转屏幕 / Activity 重建 / 切后台 = 恢复立即失败,用户只能手动刷新重来;
  2. 旧实例迟到的 onDispose 事件可覆盖新实例状态(cookie 竞态的根源之一);
  3. flight id 与 UI 实例无隔离,旧 WebView 回调可污染新实例。

核心改动

1. Host 注册与身份隔离(WafChallengeCoordinator.kt

  • 新增 WafHostRegistration 不透明令牌:每次 UI 挂载 registerHost() 领取一个;
  • setHostAvailability(registration, mounted, isForeground) / unregisterHost(registration) 按身份操作,带 generation 计数器;
  • 注销后同一令牌的后续调用被忽略——旧 Activity 的迟到事件无法影响新实例。

2. 两层模型:ActiveFlight(会话)+ ActiveAttempt(尝试)

  • ActiveFlight:一次挑战 → 一个会话。持有挑战快照、CompletableDeferred 结果(并发请求共享)、当前 attempt、runner 协程。存活到出结果或 client 关闭;
  • ActiveAttempt:会话内的一次具体浏览器尝试。绑定 host 令牌 + generation 快照 + 唯一递增 attempt id。Host 丢失即作废,回来换新 id 重建。

3. 单消费者事件循环

CookieSubmitted / AttemptFailed / HostChanged 统一走 Channel,由 flight runner 的 select 循环消费;attempt id 永不重用 + generation 只增不减,陈旧事件天然失配被丢弃。

4. 预算与超时

  • 挑战预算只扣 attempt 实际存活时间,Host 中断期间暂停
  • 新增 hostWaitTimeoutMillis(默认 30s,校验 100ms–10min):每一段连续 Host 中断有独立上限;
  • verifier 用剩余预算单独约束;验证阶段与 UI 无关。

5. 关闭语义

close() 幂等(@Volatile closed),任意阶段以 CANCELLED 完成 flight 并取消 runner;关闭后 resolve() 立即返回 CANCELLED 不挂起。

6. 其他文件

文件 改动
WafRecoveryModels.kt 新增 hostWaitTimeoutMillisWafHostState 新增 WaitingForHostVerifying 携带 host
YamiboWafChallengeHost.kt 注册/注销身份令牌;渲染前校验 verifying.host === registration;对外签名不变
WafChallengeCoordinatorTest.kt 新增/改写 ~10 个场景测试
README.md 双语更新:client 必须 outlive UI(应用级单例示例)
CHANGELOG.md / build.gradle.kts 记录变更,版本 1.1.28

新流程

flowchart TD
    A[检测到 WAF 挑战<br/>resolve] --> B{enabled?}
    B -- 否 --> C[FOREGROUND_REQUIRED]
    B -- 是 --> D{closed?}
    D -- 是 --> E[CANCELLED]
    D -- 否 --> F{锁内二次检查 closed<br/>已有活跃 Flight?}
    F -- 是 --> J[加入现有 Flight<br/>await 同一 result]
    F -- 否 --> G[创建 Flight<br/>WaitingForHost<br/>启动 runner + drainPendingEvents]
    J --> RES[WafResolution]
    G --> W1[awaitUsableHost<br/>等待可用 Host]
    W1 -- 等到 Host --> V[创建 Attempt<br/>绑定 host 令牌 + generation<br/>Verifying]
    W1 -- 30s 到期无 Host --> C
    V --> L[awaitAttemptOutcome<br/>select 事件流<br/>截止 = 剩余挑战预算]
    L -- HostLost --> P[销毁 Attempt<br/>暂停预算<br/>WaitingForHost]
    P --> W1
    L -- CookieSubmitted --> N[checkingCookie<br/>销毁 WebView<br/>写入 NOX]
    N --> O[剩余预算内 verifier]
    O -- 通过 --> OK[Verified]
    O -- 失败/超时 --> F2[VERIFICATION_FAILED / TIMED_OUT<br/>清除 NOX]
    L -- AttemptFailed --> F3[对应 disposition]
    L -- 预算耗尽 --> T2[TIMED_OUT]
    L -- close 取消 --> E2[CANCELLED]
    OK --> RES
    F2 --> RES
    F3 --> RES
    T2 --> RES
    E2 --> RES
Loading

场景行为对照

场景 旧行为 新行为
挑战中切后台 Flight 立即失败 销毁 attempt、暂停预算,回来重建 attempt 继续
旋转屏幕 / Activity 重建 恢复失败 + 旧实例事件污染新实例 旧 attempt 作废,新 Host 接棒,迟到事件被隔离
挑战到达时无 Host 立即 FOREGROUND_REQUIRED 等最多 30s,期间 Host 到场则正常恢复
验证阶段切后台 Flight 失败 不受影响,后台也能完成恢复
切后台超过 30s —(早已失败) FOREGROUND_REQUIRED,回来需刷新
快速前后台抖动 状态被覆盖 每次切换生成新 attempt,陈旧回调按 id 丢弃
多 Host(多窗口) 全局布尔量互相干扰 最新可用者胜出;工作 Host 退场时其他 Host 接力
任意时刻 close() 仅活动 flight 失败 幂等;等待中/验证中的 flight 一并 CANCELLED
单个等待者被取消 可能牵连 只取消自身,其余等待者继续共享 flight

影响与兼容性

  • YamiboWafChallengeHost 对外签名不变,调用方零改动;
  • ⚠️ client 生命周期约定变更:client 持有 flight,必须 outlive UI(文档示例已改为应用级单例);沿用旧"composable 内创建+销毁"模式会导致重建时 flight 被取消;
  • ⚠️ Headless / 纯后台客户端:无 Host 时每个挑战从"立即失败"变为挂起 30s,建议调小 hostWaitTimeoutMillisenabled = false
  • ⚠️ WafRecoveryConfig 为公开 data class,新增构造参数:源码兼容、二进制需重新编译;
  • ℹ️ 每次 attempt 新建一个 WebView,反复前后台会带来重建开销(挑战本就罕见,影响可忽略)。

测试覆盖

新增/改写测试验证:无 Host 先等后成功、Host 等待超时、前后台切换保 flight + 预算暂停(400ms 预算 vs 500ms 中断)、陈旧 detach 不能复活旧 Host、陈旧 attempt 回调被忽略、快速前后台切换开新 attempt、飞行中中断超时、verifier 取消归为失败、关闭取消等待中 flight、关闭后 resolve 立即返回。

已知风险(可跟进项)

  1. 病态 Host 高频抖动下总墙钟时间理论无界(每段中断有界但段数无界,实际不可达);
  2. 新测试依赖真实 delay 时序断言,慢速 CI 上有偶发抖动可能;
  3. headless 场景 30s 等待需在文档中明确提示。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant