返回 DeepSeek-Reasonix
endpoint.go
根目录 / internal / browser / cdp / endpoint.go
1 package cdp
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net"
9 "net/http"
10 "net/url"
11 "strings"
12 "time"
13 )
14
15 const versionProbeLimit = 1 << 20
16
17 // resolveWebSocketURL turns a user-supplied endpoint into the browser-level
18 // DevTools socket URL. A ws:// or wss:// endpoint is used as given; anything
19 // else is probed at /json/version. A DevTools endpoint hands out full control
20 // of the browser and of every file it can read, so a non-loopback host is
21 // refused unless the caller opted in.
22 func resolveWebSocketURL(ctx context.Context, endpoint string, client *http.Client, allowRemote bool) (string, error) {
23 raw := strings.TrimSpace(endpoint)
24 if raw == "" {
25 return "", fmt.Errorf("cdp: endpoint is empty")
26 }
27 if !strings.Contains(raw, "://") {
28 raw = "http://" + raw
29 }
30 u, err := url.Parse(raw)
31 if err != nil {
32 return "", fmt.Errorf("cdp: parse endpoint %q: %w", endpoint, err)
33 }
34 switch u.Scheme {
35 case "ws", "wss":
36 if err := checkHost(u, allowRemote); err != nil {
37 return "", err
38 }
39 return u.String(), nil
40 case "http", "https":
41 default:
42 return "", fmt.Errorf("cdp: endpoint %q must use http, https, ws, or wss", endpoint)
43 }
44 if err := checkHost(u, allowRemote); err != nil {
45 return "", err
46 }
47 wsURL, err := probeVersion(ctx, u, client)
48 if err != nil {
49 return "", err
50 }
51 parsed, err := url.Parse(wsURL)
52 if err != nil {
53 return "", fmt.Errorf("cdp: %s returned an unusable socket URL %q: %w", u.Host, wsURL, err)
54 }
55 if err := checkHost(parsed, allowRemote); err != nil {
56 return "", err
57 }
58 return parsed.String(), nil
59 }
60
61 // probeVersion reads webSocketDebuggerUrl from /json/version.
62 func probeVersion(ctx context.Context, base *url.URL, client *http.Client) (string, error) {
63 if client == nil {
64 client = &http.Client{Timeout: 10 * time.Second}
65 }
66 probe := *base
67 probe.Path = strings.TrimSuffix(probe.Path, "/") + "/json/version"
68 req, err := http.NewRequestWithContext(ctx, http.MethodGet, probe.String(), nil)
69 if err != nil {
70 return "", fmt.Errorf("cdp: probe %s: %w", probe.String(), err)
71 }
72 resp, err := client.Do(req)
73 if err != nil {
74 return "", fmt.Errorf("cdp: no DevTools endpoint at %s: %w", base.Host, err)
75 }
76 defer resp.Body.Close()
77 data, err := io.ReadAll(io.LimitReader(resp.Body, versionProbeLimit))
78 if err != nil {
79 return "", fmt.Errorf("cdp: probe %s: %w", probe.String(), err)
80 }
81 if resp.StatusCode != http.StatusOK {
82 return "", fmt.Errorf("cdp: probe %s: status %d", probe.String(), resp.StatusCode)
83 }
84 var out struct {
85 WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
86 }
87 if err := json.Unmarshal(data, &out); err != nil {
88 return "", fmt.Errorf("cdp: probe %s: %w", probe.String(), err)
89 }
90 if strings.TrimSpace(out.WebSocketDebuggerURL) == "" {
91 return "", fmt.Errorf("cdp: %s reported no webSocketDebuggerUrl; start Chrome with --remote-debugging-port", base.Host)
92 }
93 return out.WebSocketDebuggerURL, nil
94 }
95
96 // checkHost keeps the control socket on the loopback interface unless the
97 // caller explicitly allowed a remote one.
98 func checkHost(u *url.URL, allowRemote bool) error {
99 if allowRemote {
100 return nil
101 }
102 host := u.Hostname()
103 if host == "localhost" {
104 return nil
105 }
106 ip := net.ParseIP(host)
107 if ip != nil && ip.IsLoopback() {
108 return nil
109 }
110 return fmt.Errorf("cdp: endpoint host %q is not loopback; a DevTools endpoint grants full control of the browser and its files", host)
111 }
112
113 // waitForEndpoint polls until the DevTools socket URL resolves or ctx ends,
114 // which is how a freshly launched Chrome is met without a fixed sleep.
115 func waitForEndpoint(ctx context.Context, endpoint string, client *http.Client, allowRemote bool) (string, error) {
116 var lastErr error
117 for {
118 wsURL, err := resolveWebSocketURL(ctx, endpoint, client, allowRemote)
119 if err == nil {
120 return wsURL, nil
121 }
122 lastErr = err
123 select {
124 case <-ctx.Done():
125 return "", fmt.Errorf("cdp: browser did not expose a DevTools endpoint: %w", lastErr)
126 case <-time.After(100 * time.Millisecond):
127 }
128 }
129 }
130
130 lines GO