mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
264 lines
7.2 KiB
Go
264 lines
7.2 KiB
Go
package backend
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gorilla/websocket"
|
||
)
|
||
|
||
const (
|
||
cdpHTTPTimeout = 3 * time.Second
|
||
cdpWebSocketHandshakeTimeout = 3 * time.Second
|
||
cdpWebSocketReadTimeout = 5 * time.Second
|
||
)
|
||
|
||
// ============================================================================
|
||
// Cookie 管理 API(通过 CDP)
|
||
// ============================================================================
|
||
|
||
// CookieInfo 表示单条浏览器 Cookie
|
||
type CookieInfo struct {
|
||
Name string `json:"name"`
|
||
Value string `json:"value"`
|
||
Domain string `json:"domain"`
|
||
Path string `json:"path"`
|
||
Expires float64 `json:"expires"`
|
||
HttpOnly bool `json:"httpOnly"`
|
||
Secure bool `json:"secure"`
|
||
SameSite string `json:"sameSite"`
|
||
}
|
||
|
||
// cdpTarget 表示 /json 接口返回的调试目标
|
||
type cdpTarget struct {
|
||
WebSocketDebuggerUrl string `json:"webSocketDebuggerUrl"`
|
||
Type string `json:"type"`
|
||
}
|
||
|
||
type cdpBrowserVersion struct {
|
||
WebSocketDebuggerUrl string `json:"webSocketDebuggerUrl"`
|
||
}
|
||
|
||
// cdpMessage 是 CDP 协议消息结构
|
||
type cdpMessage struct {
|
||
Id int `json:"id"`
|
||
Method string `json:"method,omitempty"`
|
||
Params map[string]any `json:"params,omitempty"`
|
||
}
|
||
|
||
// cdpResponse 是 CDP 协议响应结构
|
||
type cdpResponse struct {
|
||
Id int `json:"id"`
|
||
Result map[string]any `json:"result,omitempty"`
|
||
Error *struct {
|
||
Message string `json:"message"`
|
||
} `json:"error,omitempty"`
|
||
}
|
||
|
||
func cdpGetEndpointBody(debugPort int, endpoint string) ([]byte, error) {
|
||
client := &http.Client{Timeout: cdpHTTPTimeout}
|
||
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d%s", debugPort, endpoint))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("unexpected status: %s", resp.Status)
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||
}
|
||
return body, nil
|
||
}
|
||
|
||
func cdpDialWebSocket(wsURL string) (*websocket.Conn, error) {
|
||
dialer := *websocket.DefaultDialer
|
||
dialer.HandshakeTimeout = cdpWebSocketHandshakeTimeout
|
||
conn, _, err := dialer.Dial(wsURL, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return conn, nil
|
||
}
|
||
|
||
// cdpCall 向指定 debugPort 发送单次 CDP 命令并返回 result 字段
|
||
func cdpCall(debugPort int, method string, params map[string]any) (map[string]any, error) {
|
||
// 1. 获取 WebSocket 调试地址
|
||
body, err := cdpGetEndpointBody(debugPort, "/json")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("CDP /json 请求失败: %w", err)
|
||
}
|
||
|
||
var targets []cdpTarget
|
||
if err := json.Unmarshal(body, &targets); err != nil || len(targets) == 0 {
|
||
return nil, fmt.Errorf("CDP targets 解析失败或为空")
|
||
}
|
||
|
||
wsURL := ""
|
||
for _, t := range targets {
|
||
if t.Type == "page" && t.WebSocketDebuggerUrl != "" {
|
||
wsURL = t.WebSocketDebuggerUrl
|
||
break
|
||
}
|
||
}
|
||
if wsURL == "" && targets[0].WebSocketDebuggerUrl != "" {
|
||
wsURL = targets[0].WebSocketDebuggerUrl
|
||
}
|
||
if wsURL == "" {
|
||
return nil, fmt.Errorf("未找到可用的 WebSocket 调试地址")
|
||
}
|
||
|
||
// 2. 建立 WebSocket 连接
|
||
conn, err := cdpDialWebSocket(wsURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("WebSocket 连接失败: %w", err)
|
||
}
|
||
defer conn.Close()
|
||
conn.SetReadDeadline(time.Now().Add(cdpWebSocketReadTimeout))
|
||
|
||
// 3. 发送 CDP 命令
|
||
msg := cdpMessage{Id: 1, Method: method, Params: params}
|
||
if err := conn.WriteJSON(msg); err != nil {
|
||
return nil, fmt.Errorf("CDP 命令发送失败: %w", err)
|
||
}
|
||
|
||
// 4. 等待响应
|
||
var cdpResp cdpResponse
|
||
if err := conn.ReadJSON(&cdpResp); err != nil {
|
||
return nil, fmt.Errorf("CDP 响应读取失败: %w", err)
|
||
}
|
||
if cdpResp.Error != nil {
|
||
return nil, fmt.Errorf("CDP 错误: %s", cdpResp.Error.Message)
|
||
}
|
||
return cdpResp.Result, nil
|
||
}
|
||
|
||
func cdpBrowserCall(debugPort int, method string, params map[string]any) error {
|
||
body, err := cdpGetEndpointBody(debugPort, "/json/version")
|
||
if err != nil {
|
||
return fmt.Errorf("CDP /json/version 请求失败: %w", err)
|
||
}
|
||
var version cdpBrowserVersion
|
||
if err := json.Unmarshal(body, &version); err != nil {
|
||
return fmt.Errorf("CDP browser target 解析失败: %w", err)
|
||
}
|
||
wsURL := strings.TrimSpace(version.WebSocketDebuggerUrl)
|
||
if wsURL == "" {
|
||
return fmt.Errorf("未找到浏览器级 WebSocket 调试地址")
|
||
}
|
||
|
||
conn, err := cdpDialWebSocket(wsURL)
|
||
if err != nil {
|
||
return fmt.Errorf("浏览器级 WebSocket 连接失败: %w", err)
|
||
}
|
||
defer conn.Close()
|
||
conn.SetReadDeadline(time.Now().Add(cdpWebSocketReadTimeout))
|
||
|
||
msg := cdpMessage{Id: 1, Method: method, Params: params}
|
||
if err := conn.WriteJSON(msg); err != nil {
|
||
return fmt.Errorf("浏览器级 CDP 命令发送失败: %w", err)
|
||
}
|
||
|
||
var cdpResp cdpResponse
|
||
if err := conn.ReadJSON(&cdpResp); err != nil {
|
||
// Browser.close 可能会直接关闭 websocket,视为成功。
|
||
if strings.EqualFold(method, "Browser.close") {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("浏览器级 CDP 响应读取失败: %w", err)
|
||
}
|
||
if cdpResp.Error != nil {
|
||
return fmt.Errorf("浏览器级 CDP 错误: %s", cdpResp.Error.Message)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// getDebugPort 获取运行中实例的调试端口
|
||
func (a *App) getDebugPort(profileId string) (int, error) {
|
||
a.browserMgr.Mutex.Lock()
|
||
defer a.browserMgr.Mutex.Unlock()
|
||
profile, exists := a.browserMgr.Profiles[profileId]
|
||
if !exists {
|
||
return 0, fmt.Errorf("profile not found: %s", profileId)
|
||
}
|
||
if !profile.Running {
|
||
return 0, fmt.Errorf("实例未运行")
|
||
}
|
||
if profile.DebugPort == 0 || !profile.DebugReady {
|
||
return 0, fmt.Errorf("实例调试接口尚未就绪,请稍后重试")
|
||
}
|
||
return profile.DebugPort, nil
|
||
}
|
||
|
||
// BrowserGetCookies 通过 CDP 获取实例所有 Cookie
|
||
func (a *App) BrowserGetCookies(profileId string) ([]CookieInfo, error) {
|
||
debugPort, err := a.getDebugPort(profileId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
result, err := cdpCall(debugPort, "Network.getAllCookies", nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
cookiesRaw, ok := result["cookies"]
|
||
if !ok {
|
||
return []CookieInfo{}, nil
|
||
}
|
||
|
||
// 通过 JSON 二次解析
|
||
data, _ := json.Marshal(cookiesRaw)
|
||
var cookies []CookieInfo
|
||
if err := json.Unmarshal(data, &cookies); err != nil {
|
||
return nil, fmt.Errorf("Cookie 解析失败: %w", err)
|
||
}
|
||
return cookies, nil
|
||
}
|
||
|
||
// BrowserClearCookies 通过 CDP 清除实例所有 Cookie
|
||
func (a *App) BrowserClearCookies(profileId string) error {
|
||
debugPort, err := a.getDebugPort(profileId)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_, err = cdpCall(debugPort, "Network.clearBrowserCookies", nil)
|
||
return err
|
||
}
|
||
|
||
// BrowserExportCookies 导出 Netscape 格式 Cookie 字符串
|
||
func (a *App) BrowserExportCookies(profileId string) (string, error) {
|
||
cookies, err := a.BrowserGetCookies(profileId)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString("# Netscape HTTP Cookie File\n")
|
||
sb.WriteString("# Generated by BrowserManager\n\n")
|
||
|
||
for _, c := range cookies {
|
||
includeSubdomains := "FALSE"
|
||
if strings.HasPrefix(c.Domain, ".") {
|
||
includeSubdomains = "TRUE"
|
||
}
|
||
secure := "FALSE"
|
||
if c.Secure {
|
||
secure = "TRUE"
|
||
}
|
||
expires := int64(c.Expires)
|
||
if expires < 0 {
|
||
expires = 0
|
||
}
|
||
sb.WriteString(fmt.Sprintf("%s\t%s\t%s\t%s\t%d\t%s\t%s\n",
|
||
c.Domain, includeSubdomains, c.Path, secure, expires, c.Name, c.Value))
|
||
}
|
||
return sb.String(), nil
|
||
}
|