package browser import ( "encoding/json" "fmt" "net" "net/http" "net/url" "strings" ) // cdpTarget representa un target CDP del endpoint /json. type cdpTarget struct { ID string `json:"id"` Type string `json:"type"` Title string `json:"title"` URL string `json:"url"` WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` } // cdpGetPageWSURL obtiene el webSocketDebuggerUrl de la primera tab de tipo "page" // via el endpoint /json. Si no hay ninguna, crea una nueva con /json/new. func cdpGetPageWSURL(host string, port int) (string, error) { if host == "" { host = "localhost" } resp, err := http.Get(fmt.Sprintf("http://%s:%d/json", host, port)) if err != nil { return "", fmt.Errorf("cdp targets: %w", err) } defer resp.Body.Close() var targets []cdpTarget if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil { return "", fmt.Errorf("cdp targets: decode: %w", err) } // Buscar la primera tab de tipo "page" for _, t := range targets { if t.Type == "page" && t.WebSocketDebuggerURL != "" { return t.WebSocketDebuggerURL, nil } } // No hay tabs — crear una nueva via /json/new newResp, err := http.Get(fmt.Sprintf("http://%s:%d/json/new", host, port)) if err != nil { return "", fmt.Errorf("cdp new tab: %w", err) } defer newResp.Body.Close() var newTarget cdpTarget if err := json.NewDecoder(newResp.Body).Decode(&newTarget); err != nil { return "", fmt.Errorf("cdp new tab: decode: %w", err) } if newTarget.WebSocketDebuggerURL == "" { return "", fmt.Errorf("cdp new tab: webSocketDebuggerUrl vacio") } return newTarget.WebSocketDebuggerURL, nil } // CdpConnect se conecta al endpoint CDP en localhost:{port} y retorna una CDPConn lista para usar. // Conecta a la primera tab de tipo "page" (que soporta Page.*, Runtime.*, Input.*). // Si no hay tabs disponibles, crea una nueva via /json/new. // Realiza el handshake WebSocket RFC 6455 sobre TCP puro (sin dependencias externas). func CdpConnect(port int) (*CDPConn, error) { return CdpConnectHost("localhost", port) } // CdpConnectHost es como CdpConnect pero permite especificar el host. // Util para WSL2 donde Chrome Windows escucha en una IP distinta a localhost. func CdpConnectHost(host string, port int) (*CDPConn, error) { if host == "" { host = "localhost" } wsURL, err := cdpGetPageWSURL(host, port) if err != nil { return nil, fmt.Errorf("cdp connect: %w", err) } // Parsear la URL del WebSocket para extraer host y path u, err := url.Parse(wsURL) if err != nil { return nil, fmt.Errorf("cdp connect: parsear ws url %q: %w", wsURL, err) } wsHost := u.Host if !strings.Contains(wsHost, ":") { wsHost = wsHost + ":80" } // Abrir conexion TCP tcpConn, err := net.Dial("tcp", wsHost) if err != nil { return nil, fmt.Errorf("cdp connect: tcp dial %s: %w", wsHost, err) } // Realizar handshake WebSocket path := u.RequestURI() reader, err := wsHandshake(tcpConn, wsHost, path) if err != nil { tcpConn.Close() return nil, fmt.Errorf("cdp connect: ws handshake: %w", err) } c := &CDPConn{ conn: tcpConn, reader: reader, port: port, pending: make(map[int64]chan cdpResponse), } // Iniciar goroutine de lectura go c.readLoop() return c, nil }