package gateway import ( "net/http" "net/http/httptest" "strings" "testing" "time" ) // TestBrowserViewerIsProxiedAndAuthenticated 验证服务端浏览器 viewer 的反代。 // // viewer 一定要经网关反代而不是让用户直连 websockify 端口: // - 直连等于把一个"已登录用户第三方账号"的浏览器暴露在网络上 // - noVNC 的核心是 WebSocket 升级,反代由 httputil 原生处理 // - 同源让 noVNC 的 WS 路径不需要跨域配置 func TestBrowserViewerIsProxiedAndAuthenticated(t *testing.T) { g := newTestGateway(t) c := g.core ps := c.Plugins() if ps == nil { t.Fatal("无 plugins") } ps.LoadSource("bp", `local plugin = { name = "bp", version = "1.0" } return plugin`) ps.SetVNCEnabled(true) sess, err := ps.StartBrowserSession("bp", "example", "about:blank", 0) if err != nil { t.Fatalf("启动浏览器会话: %v", err) } defer ps.StopBrowserSession(sess.ID) time.Sleep(4 * time.Second) // 让 websockify 起来 ts := httptest.NewServer(g.Handler()) defer ts.Close() // 不带 key → 401(viewer 继承网关鉴权) // 关键:不要跟随重定向,否则拿到的是登录页(200)而不是真实判定 noFollow := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} resp, err := noFollow.Get(ts.URL + sess.ViewURL) if err != nil { t.Fatal(err) } resp.Body.Close() t.Logf("未鉴权访问: status=%d location=%q", resp.StatusCode, resp.Header.Get("Location")) if resp.StatusCode == 200 { t.Errorf("★ 未鉴权访问 viewer 竟然 200") } // 带 admin key → 应拿到 noVNC 页面 req, _ := http.NewRequest("GET", ts.URL+"/browser/"+sess.ID+"/vnc.html", nil) req.Header.Set("Authorization", "Bearer sk-test") resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() buf := make([]byte, 4096) n, _ := resp.Body.Read(buf) body := string(buf[:n]) if resp.StatusCode != 200 { t.Fatalf("viewer 应 200, 实得 %d: %s", resp.StatusCode, body) } if !strings.Contains(strings.ToLower(body), "novnc") && !strings.Contains(body, "