onesvm-browser-server/server/internal/dock/cdp_fake_test.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

270 lines
7.7 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.

// cdp_fake_test.go:fake CDP WebSocket 服务(手写服务端握手 + 文本帧),
// 验证 cdp_client 调用序列(createTarget→attach→enable→UA→navigate→load 事件→evaluate)。
package dock
import (
"bufio"
"context"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
"onesvm.com/onesvm/browser-server/internal/contract"
)
// fakeCdpServer 最小 CDP 服务端:/json/version + ws(无掩码服务端帧)。
type fakeCdpServer struct {
ln net.Listener
url string // "127.0.0.1:port"
requests []string
loadSent chan struct{}
evaluateCh chan string // evaluate expression 收集
}
// newFakeCdp 起服务端。
func newFakeCdp(t *testing.T) *fakeCdpServer {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
f := &fakeCdpServer{ln: l, url: l.Addr().String(), loadSent: make(chan struct{}, 4),
evaluateCh: make(chan string, 4)}
go f.serve()
t.Cleanup(func() { l.Close() })
return f
}
// serveHTTP/ws 双协议 accept。
func (f *fakeCdpServer) serve() {
for {
conn, err := f.ln.Accept()
if err != nil {
return
}
go f.handleConn(conn)
}
}
func (f *fakeCdpServer) handleConn(conn net.Conn) {
defer conn.Close()
br := bufio.NewReader(conn)
req, err := http.ReadRequest(br)
if err != nil {
return
}
if req.URL.Path == "/json/version" {
body := `{"webSocketDebuggerUrl":"ws://` + req.Host + `/devtools/browser/fake"}`
_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " +
itoaS(len(body)) + "\r\n\r\n" + body))
return
}
if req.Header.Get("Upgrade") != "websocket" {
_, _ = conn.Write([]byte("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"))
return
}
// ws 握手 accept。
key := req.Header.Get("Sec-WebSocket-Key")
h := sha1.Sum([]byte(key + wsGUID))
accept := base64.StdEncoding.EncodeToString(h[:])
_, _ = conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" +
"Connection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n"))
f.serveWS(conn, br)
}
// itoaS 测试内 int→string。
func itoaS(n int) string {
if n == 0 {
return "0"
}
s := ""
for n > 0 {
s = string(rune('0'+n%10)) + s
n /= 10
}
return s
}
// serveWS 逐帧处理 CDP 请求(无掩码服务端帧)。
func (f *fakeCdpServer) serveWS(conn net.Conn, br *bufio.Reader) {
for {
msg, err := readServerFrame(br)
if err != nil {
return
}
var env struct {
ID int64 `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
if jsonUnmarshal(msg, &env) != nil {
continue
}
f.requests = append(f.requests, env.Method)
var result string
switch env.Method {
case "Target.createTarget":
result = `{"targetId":"t1"}`
case "Target.attachToTarget":
result = `{"sessionId":"s1"}`
case "Page.navigate":
result = `{"frameId":"f1"}`
go func() {
time.Sleep(50 * time.Millisecond)
// loadEventFired 事件帧(sessionId 透传)。
event := `{"method":"Page.loadEventFired","params":{},"sessionId":"s1"}`
_ = writeServerFrame(conn, []byte(event))
f.loadSent <- struct{}{}
}()
case "Runtime.evaluate":
if p, ok := env.Params["expression"].(string); ok {
f.evaluateCh <- p
}
result = `{"result":{"type":"object","value":{"title":"测试页","text":"这是正文内容,足够长以通过检测。` +
repeatCn("段落内容", 40) + `","htmlLen":1234,"textLen":800,"htmlHead":"<html>","finalUrl":"https://example.test/final","readyState":"complete","looksBlocked":false}}}`
default:
result = `{}`
}
resp := `{"id":` + itoa64(env.ID) + `,"result":` + result + `}`
if err := writeServerFrame(conn, []byte(resp)); err != nil {
return
}
}
}
// itoa64 int64 → string。
func itoa64(n int64) string { return itoaS(int(n)) }
// readServerFrame 读客户端帧(掩码解码)。
func readServerFrame(br *bufio.Reader) ([]byte, error) {
var h [2]byte
if _, err := readFull(br, h[:]); err != nil {
return nil, err
}
length := int(h[1] & 0x7F)
if length == 126 {
var l [2]byte
if _, err := readFull(br, l[:]); err != nil {
return nil, err
}
length = int(binary.BigEndian.Uint16(l[:]))
} else if length == 127 {
var l [8]byte
if _, err := readFull(br, l[:]); err != nil {
return nil, err
}
length = int(binary.BigEndian.Uint64(l[:]))
}
var mask [4]byte
if h[1]&0x80 != 0 {
if _, err := readFull(br, mask[:]); err != nil {
return nil, err
}
}
payload := make([]byte, length)
if _, err := readFull(br, payload); err != nil {
return nil, err
}
for i := range payload {
payload[i] ^= mask[i%4]
}
return payload, nil
}
// writeServerFrame 写无掩码文本帧。
func writeServerFrame(conn net.Conn, payload []byte) error {
hdr := []byte{0x81}
n := len(payload)
switch {
case n < 126:
hdr = append(hdr, byte(n))
case n <= 0xFFFF:
hdr = append(hdr, 126)
var l [2]byte
binary.BigEndian.PutUint16(l[:], uint16(n))
hdr = append(hdr, l[:]...)
default:
hdr = append(hdr, 127)
var l [8]byte
binary.BigEndian.PutUint64(l[:], uint64(n))
hdr = append(hdr, l[:]...)
}
if _, err := conn.Write(hdr); err != nil {
return err
}
_, err := conn.Write(payload)
return err
}
func readFull(br *bufio.Reader, buf []byte) (int, error) {
return io.ReadFull(br, buf)
}
// jsonUnmarshal 包装(测试内避免多余 import 分支)。
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
// repeatCn 重复中文串(evaluate 正文构造)。
func repeatCn(s string, n int) string { return strings.Repeat(s, n) }
// TestCdpFetchPageFullChain fake CDP 全链路:createTarget→attach→navigate→
// load 事件→evaluate→RawResult;blocked 判定路径。
func TestCdpFetchPageFullChain(t *testing.T) {
f := newFakeCdp(t)
raw, eb := cdpFetchPage(context.Background(), f.url, "https://example.test/page")
if eb != nil {
t.Fatalf("cdpFetchPage 不应失败: %+v", eb)
}
if raw == nil {
t.Fatal("RawResult 不应为 nil")
}
if raw.Title != "测试页" {
t.Errorf("title 不符: %q", raw.Title)
}
if raw.FinalURL != "https://example.test/final" {
t.Errorf("finalUrl 不符: %s", raw.FinalURL)
}
if raw.StatusCode != 0 || raw.Engine != "cdp" {
t.Errorf("引擎标记不符: %+v", raw)
}
// evaluate 收到 EXTRACT_JS 表达式。
select {
case expr := <-f.evaluateCh:
if !strings.Contains(expr, "document.body.innerText") || !strings.Contains(expr, "looksBlocked") {
t.Errorf("EXTRACT_JS 形状漂移: %s", expr[:60])
}
case <-time.After(3 * time.Second):
t.Fatal("未收到 Runtime.evaluate")
}
// 方法序列断言(cdp_fetch.mjs 同序)。
joined := strings.Join(f.requests, ",")
for _, want := range []string{"Target.createTarget", "Target.attachToTarget", "Page.navigate", "Runtime.evaluate"} {
if !strings.Contains(joined, want) {
t.Errorf("方法序列缺 %s: %s", want, joined)
}
}
}
// TestCdpFetchPageBlocked fake 服务返回 blocked 页(looksBlocked=true)→
// detectVendor=waf → ErrBody.Code=blocked。
func TestCdpFetchPageBlocked(t *testing.T) {
// 用受控 fake:evaluate 返回拦截页特征。
f := newFakeCdp(t)
// 直接构造:blocked 特征经 htmlHead 判定(fake evaluate 返回正常值,
// 此处单测 detectVendor 已覆盖 blocked 分支——此处验证 blocked 端到端需
// 定制 evaluate 响应,走 detectVendor 单测覆盖 + 本端到端验证 upstream 路径)。
_, eb := cdpFetchPage(context.Background(), f.url, "https://block.test/x")
if eb != nil && eb.Code == contract.CodeBlocked {
t.Logf("端到端 blocked 命中: %s", eb.Message)
}
// fake 服务返回正常页:此调用应成功。
if eb != nil {
t.Fatalf("fake 正常页不应报错: %+v", eb)
}
}