package core import ( "net/url" "regexp" "strings" "testing" "llmsproxy/internal/lua" ) // callback 平台的授权 URL 是最容易"看起来对、实际不可用"的地方:URL 能拼出来、 // 页面能打开,但参数名/编码/主机错一处,上游就拒绝授权。 // // 判据照参考实现(trae-oauth.ts / gemini-oauth.ts / lobsterai-oauth.ts)逐条锁定: // - Trae 参数名必须是 auth_callback_url(不是 redirect_uri) // - Gemini redirect_uri 主机必须是 localhost(不是 127.0.0.1),token 交换要一致 // - LobsterAI 的 login 在 **hash 段**(/portal#/login),三个参数用 ? 起 // - redirect_uri 必须百分号编码(含 :// 与 :) func TestCallbackAuthorizeUrls(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := readBundledChannels(t) if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } start := func(t *testing.T, plat string) (string, map[string]interface{}) { t.Helper() out, err := ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"` + plat + `"}`, Role: "admin", }) if err != nil { t.Fatalf("%s start_login: %v", plat, err) } j := out.JSON.(map[string]interface{}) u, _ := j["url"].(string) return u, j } t.Run("行1 trae 用 auth_callback_url 而非 redirect_uri", func(t *testing.T) { u, _ := start(t, "trae") if u == "" { t.Fatal("trae 未给出授权 URL") } if !strings.Contains(u, "auth_callback_url=") { t.Fatalf("trae 必须用 auth_callback_url(参考注释点名:错名则登录必失败): %s", u) } if strings.Contains(u, "redirect_uri=") { t.Fatalf("trae 不该有 redirect_uri 参数: %s", u) } for _, want := range []string{"client_id=en1oxy7wnw8j9n", "auth_type=local", "login_channel=native_ide"} { if !strings.Contains(u, want) { t.Fatalf("trae 缺参数 %s: %s", want, u) } } // 关键:不能再出现 "…/authorization&redirect_uri=" 这种坏的拼接 if strings.Contains(u, "authorization&") { t.Fatalf("授权 URL 首参分隔符必须是 ? 而不是 &: %s", u) } // 参考 trae-oauth.ts 把参数集列为「一个都不能少」(早期只发 5 个参数, // 登录页**永远停在授权中**,既不跳转也不回传)。逐条锁定: // login_trace_id 是回调反查 pending 的唯一凭据;machine/device 身份除外 // 还可能被风控拦;x_* 是客户端形态伪装。 parsed, err := url.Parse(u) if err != nil { t.Fatalf("trae 授权 URL 无法解析: %v", err) } q := parsed.Query() for _, k := range []string{ "login_version", "auth_from", "login_channel", "plugin_version", "auth_type", "client_id", "redirect", "login_trace_id", "auth_callback_url", "machine_id", "device_id", "x_device_id", "x_machine_id", "x_device_brand", "x_device_type", "x_os_version", "x_app_version", "x_app_type", } { if q.Get(k) == "" { t.Fatalf("trae 缺参数 %s(参考列为必需): %s", k, u) } } // 设备身份是 hex32,trace_id 是它们拼接后的末 16 位 m, d := q.Get("machine_id"), q.Get("device_id") if len(m) != 32 || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(m) { t.Fatalf("machine_id 应为 hex32: %q", m) } if len(d) != 32 || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(d) { t.Fatalf("device_id 应为 hex32: %q", d) } if m == d { t.Fatalf("machine_id 与 device_id 不应相同(同源会削弱设备身份)") } joined := m + d if got := q.Get("login_trace_id"); got != joined[len(joined)-16:] { t.Fatalf("login_trace_id 应为 (machine_id+device_id) 末16位,实得 %q", got) } // 同一平台两次登录必须得到不同设备身份(否则多账号共用设备, // 会被上游以「该设备已签到」拦截) u2, _ := start(t, "trae") q2, _ := url.Parse(u2) if q2.Query().Get("device_id") == d { t.Fatalf("两次登录的 device_id 应不同(每账号独立设备身份)") } // Trae 注册的回调路径是 /authorize(不是插件的 /api/plugins/http 挂载点): // 回环地址由「打开授权页的那台机器」解析,所以必须是本机 loopback 接收器。 if cb := q.Get("auth_callback_url"); cb != "" { if !strings.HasSuffix(cb, "/authorize") { t.Fatalf("trae auth_callback_url 必须以注册路径 /authorize 结尾,实得 %q", cb) } if !strings.HasPrefix(cb, "http://127.0.0.1:") { t.Fatalf("trae auth_callback_url 应是 127.0.0.1 回环地址,实得 %q", cb) } } }) t.Run("行2 gemini 主机是 localhost 且参数不重复", func(t *testing.T) { u, _ := start(t, "gemini") parsed, err := url.Parse(u) if err != nil { t.Fatalf("授权 URL 无法解析: %v", err) } ru := parsed.Query().Get("redirect_uri") if !strings.HasPrefix(ru, "http://localhost:") { t.Fatalf("gemini redirect_uri 主机必须是 localhost(参考注释明确): %q", ru) } // 路径必须是注册值 /oauth-callback(参考 GEMINI_CALLBACK_PATH)—— // 换成别的路径就是 redirect_uri_mismatch。 if !strings.HasSuffix(ru, "/oauth-callback") { t.Fatalf("gemini redirect_uri 路径必须是 /oauth-callback,实得 %q", ru) } for _, k := range []string{"client_id", "response_type", "scope", "state"} { if parsed.Query().Get(k) == "" { t.Fatalf("gemini 缺参数 %s: %s", k, u) } } // 缺 access_type=offline 拿不到 refresh_token;prompt=consent 保证每次都发。 if got := parsed.Query().Get("access_type"); got != "offline" { t.Fatalf("gemini access_type 应为 offline(否则无 refresh_token),实得 %q", got) } if got := parsed.Query().Get("prompt"); got != "consent" { t.Fatalf("gemini prompt 应为 consent,实得 %q", got) } // 参数不得重复(通用分支曾再追加一次 redirect_uri) if n := strings.Count(u, "redirect_uri="); n != 1 { t.Fatalf("redirect_uri 出现 %d 次(应恰好 1 次): %s", n, u) } if n := strings.Count(u, "client_id="); n != 1 { t.Fatalf("client_id 出现 %d 次: %s", n, u) } }) t.Run("行3 lobsterai 的 login 在 hash 段且参数编码", func(t *testing.T) { u, _ := start(t, "lobsterai") if !strings.Contains(u, "/portal#/login?") { t.Fatalf("lobsterai 授权页必须是 /portal#/login?…(login 在 hash 段): %s", u) } if strings.Contains(u, "/login&") { t.Fatalf("首参分隔符必须是 ? 而不是 &: %s", u) } if !strings.Contains(u, "source=electron") { t.Fatalf("lobsterai 缺 source=electron: %s", u) } // redirect_uri 必须编码(含 :// 与 :),否则登录页校验失败 if !strings.Contains(u, "redirect_uri=http%3A%2F%2F") { t.Fatalf("lobsterai redirect_uri 必须百分号编码: %s", u) } }) t.Run("行4 端口用本机监听值而非硬编码8081", func(t *testing.T) { for _, plat := range []string{"trae", "gemini", "lobsterai"} { u, _ := start(t, plat) if strings.Contains(u, "127.0.0.1:8081") && plat != "trae" { t.Fatalf("%s 不应硬编码 8081: %s", plat, u) } if strings.Contains(u, "{port}") { t.Fatalf("%s 的 {port} 占位未被替换: %s", plat, u) } } }) }