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>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user