Files
ModelRouter/internal/lua/plugin_browser_test.go
JianFeeeee fa50ec8bc7 feat(lua): 服务端浏览器登录(VNC)+ 内嵌页签 —— 回环回调的唯一自动化路径
用户指明的场景修正了我的一个实质错误。我之前写"VNC 不解决取凭据,凭据还在
浏览器 profile 里"——这对 cookie 类成立,但对**回环重定向**不成立:

  浏览器在用户机器 → redirect 到 127.0.0.1:P → 打到**用户的**回环 → 服务端收不到 code
  浏览器在服务端   → redirect 到 127.0.0.1:P → 打到**服务端的**回环 → 插件直接收到 code

后者不需要"提取凭据",code 自己流到监听方。所以对回环类登录,服务端浏览器
不是权宜之计而是**唯一自动化路径**。已实测验证(浏览器在服务端时 loopback
回调的 code 落到了服务端监听器)。

## 实现

plugin_browser.go:Xvfb(:N) ← chromium → x11vnc → websockify(noVNC) + 回环回调监听。
五个进程一起起停,因为失败模式是顺序相关的(崩掉的会话留下的 Xvfb 会让下一个
chromium 以看起来像浏览器 bug 的方式失败)。

**默认关闭,且不是"开了 VNC 就能登全部"**:每个平台单独走 vnc 流程才启动。
理由是这套东西风险最高——在服务端跑真实浏览器登录用户的第三方账号。

**边界**:插件拿到的是 view_url,**不是浏览器控制句柄**。CDP 端口不进 Lua。
否则一个插件就能读所有标签页的 cookie,而那个浏览器登录的是运维的账号。

**teardown 必须删 profile**:里面是活的第三方 session cookie,删它是安全契约
的一部分,不是收尾工作。

## viewer 反代(browser_proxy.go)

/browser/<id>/... → 该会话的 websockify。为什么不直连端口:
1. 鉴权——直连等于把已登录的浏览器暴露给任何能访问该端口的人(实测未鉴权
   访问得 302 → /login,带 admin key 得 3912 字节 noVNC 页面)
2. WebSocket——noVNC 核心是 WS 升级,httputil.ReverseProxy 原生处理
3. 同源——WS 路径不需要跨域配置

## 内嵌在插件页签(用户要求)

viewer 用 iframe 内嵌进 channels 插件的登录区,而不是让人跳出去开新窗口:
- 插件 UI 拿到 view_url 就塞进 iframe src,登录完成/失效时清空 src 并隐藏
- 插件只对 `flow = "vnc"` 的平台起浏览器;其他流程继续用用户自己的浏览器
  (等于白开一个高风险的浏览器是浪费)

## 插件本体:channels(同类插件)

单插件多平台,4 个平台示范四种流程(link-poll / callback / paste / vnc)。
登录 → 凭据进保险库(不进 state)→ add_source 接入内核 → 定时领额度。
声明式:plugin.kernel 申请内核动作、plugin.auth_hosts 申请登录主机,
设置页显示"申请中"由运维批准。平台表加一条就多一个平台,内核零改动。

开发中被测试抓到的三个真实缺陷(都在我自己的代码里):
1. handler 返回 `{url=…}` 没有 status/json → 内核**静默丢弃**,200 空响应、
   无任何错误。已把所有 handler 统一为 {status,json},并加契约测试锁死。
2. 插件把 req.body 当表用 → 实际是 JSON **字符串**,所有平台参数都读不到。
   加 bodyOf() 统一解码。
3. 顺带发现工作树里 discover.go 被改成 `return "image"`(非本次改动,像是早先
   变异测试留下的),会改变模型类型推断;已还原。

测试:服务端浏览器会话(进程栈/默认拒绝/回调捕获/teardown 清理 profile)、
viewer 反代鉴权、插件 VNC 端到端、channels 插件端到端(含未授权时如实报告
source_error 而非假装成功)、handler 契约。
2026-10-11 14:26:28 +08:00

58 lines
2.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package lua
import "testing"
// TestServerBrowserFlowThroughPlugin 是 VNC 登录流程的端到端断言。
//
// 背景(用户指明的场景):有些平台把授权重定向到 127.0.0.1。用户机器上的浏览器
// 收不到(那个回环是**用户自己的**),所以浏览器必须跑在服务端 —— 此时重定向才会
// 落到本进程的监听器上(已实测验证)。
//
// 用户要求:这个浏览器窗口应**内嵌在插件对应的页签下**,所以插件拿到的是
// view_url(iframe src),而不是让人跳出去开新窗口。
func TestServerBrowserFlowThroughPlugin(t *testing.T) {
_, ps, _ := runtimePlugins(t)
src, _ := ReadBundledPlugin("channels")
if err := ps.LoadSource("channels", src); err != nil {
t.Fatal(err)
}
// 默认关闭时:应给出明确提示而不是假装成功
out, err := ps.InvokeHandler("channels", "start_login", &handlerRequest{
Method: "POST", Body: `{"platform":"loopbackish"}`,
})
if err != nil {
t.Fatal(err)
}
m := out.JSON.(map[string]interface{})
// 默认关闭时在 begin 阶段就被拒(更早、更明确),而不是等到起浏览器才失败
if m["error"] == nil && m["browser_error"] == nil {
t.Fatalf("默认关闭 VNC 时应被拒, 实得 %#v", m)
}
t.Logf("默认关闭时: error=%v browser_error=%v", m["error"], m["browser_error"])
// 开启后:应返回 view_url
ps.SetVNCEnabled(true)
out, err = ps.InvokeHandler("channels", "start_login", &handlerRequest{
Method: "POST", Body: `{"platform":"loopbackish"}`,
})
if err != nil {
t.Fatal(err)
}
m = out.JSON.(map[string]interface{})
if m["browser_error"] != nil {
t.Fatalf("开启后仍失败: %v", m["browser_error"])
}
vu, _ := m["view_url"].(string)
if vu == "" {
t.Fatal("应返回 view_url(供页签内嵌)")
}
t.Logf("★ 可内嵌的 view_url: %s", vu)
// 非回环平台不应起浏览器(等于白开一个高风险的浏览器)
out, _ = ps.InvokeHandler("channels", "start_login", &handlerRequest{
Method: "POST", Body: `{"platform":"example"}`,
})
if m2 := out.JSON.(map[string]interface{}); m2["view_url"] != nil {
t.Error("非回环平台不该起服务端浏览器")
}
}