onesvm-browser-server/server/internal/dock/ws.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

224 lines
6.8 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 dock 拓展坞适配器层:五个引擎适配器(searxng-cn / searxng-global /
// trafilatura / lightpanda / headless-shell)实现 contract.DockAdapter 五方法。
//
// 复用声明:CDP 调用面(json/version → ws → Target.createTarget/attachToTarget
// → Page/Runtime/Network enable → UA override → Page.navigate → loadEventFired →
// Runtime.evaluate 提取)与 detectVendor 反爬判定正则,均移植自
// bench/site-matrix/cdp_fetch.mjs(Go 重写,面窄)。trafilatura 契约
// (POST /v1/read,响应 {ok,title,markdown,char_count,truncated})来自
// bench/trafilatura-http/app.py。searxng 参数对齐 bench/searxng-{cn,global}/settings.yml。
//
// ws.go:手写 RFC6455 最小 WebSocket 客户端帧编解码(依赖纪律禁新增第三方库:
// 只需握手 + 无压缩文本帧收发,不实现分片/子协议,面窄可控)。
package dock
import (
"bufio"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
// wsHandshakeKey GUID(RFC6455 §1.3)。
const wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// wsConn 已完成握手的 WebSocket 客户端连接(文本帧收发)。
// 并发模型:写侧串行(writeMu),读侧单 goroutine 持有。
type wsConn struct {
conn net.Conn
br *bufio.Reader
writeMu sync.Mutex
}
// wsDial 建立 WebSocket 连接:HTTP Upgrade 握手(RFC6455 client 端最小面)。
// scheme 仅支持 ws(CDP 调试端点均为明文 ws://,overlay 内网无 TLS 需求)。
func wsDial(rawURL string, timeout time.Duration) (*wsConn, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("dock: ws URL 解析: %w", err)
}
if u.Scheme != "ws" {
// 兼容 wss:本版不实现 TLS 握手(引擎端点均为内网明文)。
if u.Scheme == "wss" {
return nil, errors.New("dock: wss 不支持(引擎端点应为内网明文 ws://)")
}
return nil, fmt.Errorf("dock: 非 ws scheme: %q", u.Scheme)
}
host := u.Host
if u.Port() == "" {
host = net.JoinHostPort(u.Hostname(), "80")
}
conn, err := net.DialTimeout("tcp", host, timeout)
if err != nil {
return nil, fmt.Errorf("dock: ws dial %s: %w", host, err)
}
// 16 字节随机 key。
keyBytes := make([]byte, 16)
if _, err := rand.Read(keyBytes); err != nil {
conn.Close()
return nil, fmt.Errorf("dock: ws key 随机源: %w", err)
}
key := base64.StdEncoding.EncodeToString(keyBytes)
reqLines := []string{
"GET " + u.RequestURI() + " HTTP/1.1",
"Host: " + u.Host,
"Upgrade: websocket",
"Connection: Upgrade",
"Sec-WebSocket-Key: " + key,
"Sec-WebSocket-Version: 13",
}
upgradeReq := strings.Join(reqLines, "\r\n") + "\r\n\r\n"
if _, err := conn.Write([]byte(upgradeReq)); err != nil {
conn.Close()
return nil, fmt.Errorf("dock: ws 握手写入: %w", err)
}
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, nil)
if err != nil {
conn.Close()
return nil, fmt.Errorf("dock: ws 握手响应: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusSwitchingProtocols {
conn.Close()
return nil, fmt.Errorf("dock: ws 握手状态 %d", resp.StatusCode)
}
// Sec-WebSocket-Accept 校验(防非 ws 服务误挂)。
want := base64.StdEncoding.EncodeToString(sha1Sum(key + wsGUID))
if got := resp.Header.Get("Sec-WebSocket-Accept"); got != want {
conn.Close()
return nil, fmt.Errorf("dock: ws accept 不匹配(got=%s want=%s)", got, want)
}
return &wsConn{conn: conn, br: br}, nil
}
// sha1Sum SHA-1 摘要(RFC6455 accept 计算)。
func sha1Sum(s string) []byte {
h := sha1.Sum([]byte(s))
return h[:]
}
// writeFrame 写一帧文本(客户端帧必须掩码,RFC6455 §5.3)。
// 本面窄实现:单帧 ≤2^16-1 载荷(CDP evaluate 表达式与事件均远小于此),
// 超长自动升级 64-bit 长度字段。
func (c *wsConn) writeFrame(opcode byte, payload []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
var hdr []byte
hdr = append(hdr, 0x80|opcode) // FIN=1,无扩展位
n := len(payload)
maskBit := byte(0x80)
switch {
case n < 126:
hdr = append(hdr, maskBit|byte(n))
case n <= 0xFFFF:
hdr = append(hdr, maskBit|126)
var l [2]byte
binary.BigEndian.PutUint16(l[:], uint16(n))
hdr = append(hdr, l[:]...)
default:
hdr = append(hdr, maskBit|127)
var l [8]byte
binary.BigEndian.PutUint64(l[:], uint64(n))
hdr = append(hdr, l[:]...)
}
// 掩码 key(RFC6455 客户端强制)。
var mask [4]byte
if _, err := rand.Read(mask[:]); err != nil {
return fmt.Errorf("dock: ws 掩码随机源: %w", err)
}
hdr = append(hdr, mask[:]...)
if _, err := c.conn.Write(hdr); err != nil {
return fmt.Errorf("dock: ws 写头: %w", err)
}
masked := make([]byte, n)
for i, b := range payload {
masked[i] = b ^ mask[i%4]
}
if _, err := c.conn.Write(masked); err != nil {
return fmt.Errorf("dock: ws 写体: %w", err)
}
return nil
}
// errConnClosed 连接已关闭哨兵。
var errConnClosed = errors.New("dock: ws 连接已关闭")
// readFrame 读一帧(自动聚合分片 CONT;忽略 ping/pong/close 控制帧以外的处理:
// ping 回 pong,close 视为对端关闭)。只支持文本帧(opcode=1)产出。
func (c *wsConn) readFrame() ([]byte, error) {
var acc []byte
for {
var h [2]byte
if _, err := io.ReadFull(c.br, h[:]); err != nil {
return nil, fmt.Errorf("dock: ws 读头: %w", err)
}
fin := h[0]&0x80 != 0
opcode := h[0] & 0x0F
masked := h[1]&0x80 != 0
length := int(h[1] & 0x7F)
switch length {
case 126:
var l [2]byte
if _, err := io.ReadFull(c.br, l[:]); err != nil {
return nil, fmt.Errorf("dock: ws 读len16: %w", err)
}
length = int(binary.BigEndian.Uint16(l[:]))
case 127:
var l [8]byte
if _, err := io.ReadFull(c.br, l[:]); err != nil {
return nil, fmt.Errorf("dock: ws 读len64: %w", err)
}
length = int(binary.BigEndian.Uint64(l[:]))
}
var mask [4]byte
if masked {
if _, err := io.ReadFull(c.br, mask[:]); err != nil {
return nil, fmt.Errorf("dock: ws 读掩码: %w", err)
}
}
payload := make([]byte, length)
if _, err := io.ReadFull(c.br, payload); err != nil {
return nil, fmt.Errorf("dock: ws 读载荷: %w", err)
}
if masked {
for i := range payload {
payload[i] ^= mask[i%4]
}
}
switch opcode {
case 0x1: // 文本
acc = append(acc, payload...)
if fin {
return acc, nil
}
case 0x0: // CONT 分片
acc = append(acc, payload...)
if fin {
return acc, nil
}
case 0x8: // close
return nil, errConnClosed
case 0x9: // ping → pong(无载荷回显)
if err := c.writeFrame(0xA, payload); err != nil {
return nil, err
}
case 0xA: // pong 忽略
default:
return nil, fmt.Errorf("dock: ws 未知 opcode %d", opcode)
}
}
}
// Close 关闭底层连接。
func (c *wsConn) Close() error { return c.conn.Close() }