Files
navegator/pkg/cdp/client.go
T
Developer 3253828fef
Tests / Lint (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / E2E Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Initial commit: navegator - Chrome CDP automation for LLMs
Add complete navegator system for stealthy browser automation:
- CDP client with WebSocket communication
- Browser API with navigation, storage, network, runtime
- Stealth flags and anti-detection scripts
- Persistent profile support
- Examples and comprehensive documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-24 23:33:07 +01:00

275 lines
6.2 KiB
Go

package cdp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sync"
"sync/atomic"
"github.com/gorilla/websocket"
)
// Client representa un cliente del Chrome DevTools Protocol.
type Client struct {
wsURL string
conn *websocket.Conn
mu sync.Mutex
nextID atomic.Int64
callbacks map[int64]chan *Response
events map[string][]EventHandler
eventMu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
closeCh chan struct{}
closeOnce sync.Once
}
// EventHandler es una función que maneja eventos CDP.
type EventHandler func(params json.RawMessage)
// Request representa una solicitud CDP.
type Request struct {
ID int64 `json:"id"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
}
// Response representa una respuesta CDP.
type Response struct {
ID int64 `json:"id"`
Result json.RawMessage `json:"result,omitempty"`
Error *ErrorResponse `json:"error,omitempty"`
}
// Event representa un evento CDP.
type Event struct {
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
// ErrorResponse representa un error en una respuesta CDP.
type ErrorResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"`
}
// NewClient crea un nuevo cliente CDP conectado al WebSocket URL especificado.
func NewClient(ctx context.Context, wsURL string) (*Client, error) {
conn, _, err := websocket.DefaultDialer.DialContext(ctx, wsURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to connect to CDP websocket: %w", err)
}
clientCtx, cancel := context.WithCancel(ctx)
c := &Client{
wsURL: wsURL,
conn: conn,
callbacks: make(map[int64]chan *Response),
events: make(map[string][]EventHandler),
ctx: clientCtx,
cancel: cancel,
closeCh: make(chan struct{}),
}
// Iniciar goroutine para recibir mensajes
go c.receiveLoop()
return c, nil
}
// receiveLoop procesa mensajes entrantes del WebSocket.
func (c *Client) receiveLoop() {
defer close(c.closeCh)
for {
select {
case <-c.ctx.Done():
return
default:
}
_, data, err := c.conn.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
// Log error pero no hacer panic
}
return
}
// Intentar parsear como Response primero
var resp Response
if err := json.Unmarshal(data, &resp); err == nil && resp.ID > 0 {
c.handleResponse(&resp)
continue
}
// Sino, es un evento
var event Event
if err := json.Unmarshal(data, &event); err == nil && event.Method != "" {
c.handleEvent(&event)
}
}
}
// handleResponse procesa una respuesta CDP.
func (c *Client) handleResponse(resp *Response) {
c.mu.Lock()
ch, ok := c.callbacks[resp.ID]
if ok {
delete(c.callbacks, resp.ID)
}
c.mu.Unlock()
if ok {
select {
case ch <- resp:
default:
}
}
}
// handleEvent procesa un evento CDP.
func (c *Client) handleEvent(event *Event) {
c.eventMu.RLock()
handlers := c.events[event.Method]
c.eventMu.RUnlock()
for _, handler := range handlers {
go handler(event.Params)
}
}
// Execute envía un comando CDP y espera la respuesta.
func (c *Client) Execute(ctx context.Context, method string, params interface{}, result interface{}) error {
id := c.nextID.Add(1)
req := Request{
ID: id,
Method: method,
Params: params,
}
data, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
// Crear canal para respuesta
respCh := make(chan *Response, 1)
c.mu.Lock()
c.callbacks[id] = respCh
c.mu.Unlock()
// Enviar request
c.mu.Lock()
err = c.conn.WriteMessage(websocket.TextMessage, data)
c.mu.Unlock()
if err != nil {
c.mu.Lock()
delete(c.callbacks, id)
c.mu.Unlock()
return fmt.Errorf("failed to send request: %w", err)
}
// Esperar respuesta
select {
case resp := <-respCh:
if resp.Error != nil {
return fmt.Errorf("CDP error %d: %s - %s", resp.Error.Code, resp.Error.Message, resp.Error.Data)
}
if result != nil && len(resp.Result) > 0 {
if err := json.Unmarshal(resp.Result, result); err != nil {
return fmt.Errorf("failed to unmarshal result: %w", err)
}
}
return nil
case <-ctx.Done():
c.mu.Lock()
delete(c.callbacks, id)
c.mu.Unlock()
return ctx.Err()
case <-c.closeCh:
return errors.New("client closed")
}
}
// On registra un handler para un evento específico.
func (c *Client) On(event string, handler EventHandler) {
c.eventMu.Lock()
defer c.eventMu.Unlock()
c.events[event] = append(c.events[event], handler)
}
// Close cierra la conexión CDP.
func (c *Client) Close() error {
var err error
c.closeOnce.Do(func() {
c.cancel()
c.mu.Lock()
if c.conn != nil {
err = c.conn.Close()
}
c.mu.Unlock()
})
return err
}
// GetWebSocketURL obtiene la URL del WebSocket CDP desde el endpoint HTTP.
func GetWebSocketURL(ctx context.Context, debugURL string) (string, error) {
// Primero intentar con /json para obtener lista de targets
req, err := http.NewRequestWithContext(ctx, "GET", debugURL+"/json", nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to get CDP info: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
// /json retorna un array de targets
var targets []struct {
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
Type string `json:"type"`
}
if err := json.Unmarshal(body, &targets); err != nil {
return "", fmt.Errorf("failed to unmarshal response: %w", err)
}
// Buscar el primer target de tipo "page"
for _, target := range targets {
if target.Type == "page" && target.WebSocketDebuggerURL != "" {
return target.WebSocketDebuggerURL, nil
}
}
// Si no hay page, usar el primero disponible
if len(targets) > 0 && targets[0].WebSocketDebuggerURL != "" {
return targets[0].WebSocketDebuggerURL, nil
}
return "", errors.New("no webSocketDebuggerUrl found in targets")
}