package browser import ( "bufio" "crypto/rand" "crypto/sha1" "encoding/base64" "encoding/binary" "encoding/json" "fmt" "io" "net" "net/http" "strings" "sync" "sync/atomic" ) // CDPConn es una conexion activa al Chrome DevTools Protocol. // Gestiona el WebSocket raw y el protocolo JSON-RPC de CDP. type CDPConn struct { conn net.Conn reader *bufio.Reader mu sync.Mutex nextID atomic.Int64 port int pid int pending map[int64]chan cdpResponse pendMu sync.Mutex closed bool } type cdpRequest struct { ID int64 `json:"id"` Method string `json:"method"` Params map[string]any `json:"params,omitempty"` } type cdpResponse struct { ID int64 `json:"id"` Result map[string]any `json:"result"` Error *cdpError `json:"error"` Method string `json:"method"` // para eventos Params map[string]any `json:"params"` // para eventos } type cdpError struct { Code int `json:"code"` Message string `json:"message"` } // cdpVersionResponse es la respuesta de /json/version del endpoint CDP. type cdpVersionResponse struct { WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` Browser string `json:"Browser"` } // wsHandshake realiza el handshake WebSocket RFC 6455 sobre una conexion TCP ya abierta. func wsHandshake(conn net.Conn, host, path string) (*bufio.Reader, error) { // Generar clave aleatoria de 16 bytes en base64 keyBytes := make([]byte, 16) if _, err := rand.Read(keyBytes); err != nil { return nil, fmt.Errorf("ws handshake: generar clave: %w", err) } key := base64.StdEncoding.EncodeToString(keyBytes) // Enviar request HTTP upgrade req := fmt.Sprintf( "GET %s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n", path, host, key, ) if _, err := fmt.Fprint(conn, req); err != nil { return nil, fmt.Errorf("ws handshake: enviar upgrade: %w", err) } // Leer respuesta HTTP reader := bufio.NewReaderSize(conn, 65536) status, err := reader.ReadString('\n') if err != nil { return nil, fmt.Errorf("ws handshake: leer status: %w", err) } if !strings.Contains(status, "101") { return nil, fmt.Errorf("ws handshake: status inesperado: %s", strings.TrimSpace(status)) } // Consumir headers hasta linea vacia var acceptKey string for { line, err := reader.ReadString('\n') if err != nil { return nil, fmt.Errorf("ws handshake: leer headers: %w", err) } line = strings.TrimRight(line, "\r\n") if line == "" { break } if strings.HasPrefix(strings.ToLower(line), "sec-websocket-accept:") { acceptKey = strings.TrimSpace(line[len("sec-websocket-accept:"):]) } } // Verificar Sec-WebSocket-Accept magic := "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" h := sha1.New() h.Write([]byte(key + magic)) expected := base64.StdEncoding.EncodeToString(h.Sum(nil)) if acceptKey != expected { return nil, fmt.Errorf("ws handshake: accept key invalida: got %q, want %q", acceptKey, expected) } return reader, nil } // wsReadMessage lee un frame WebSocket y retorna el payload. // Solo soporta frames de texto/binario no fragmentados (suficiente para CDP). func wsReadMessage(reader *bufio.Reader) ([]byte, error) { // Leer primeros 2 bytes del frame header := make([]byte, 2) if _, err := io.ReadFull(reader, header); err != nil { return nil, fmt.Errorf("ws read: header: %w", err) } // fin := (header[0] & 0x80) != 0 // ignoramos fragmentacion opcode := header[0] & 0x0F masked := (header[1] & 0x80) != 0 payloadLen := int64(header[1] & 0x7F) if opcode == 8 { return nil, fmt.Errorf("ws read: connection close frame") } // Leer longitud extendida switch payloadLen { case 126: var ext uint16 if err := binary.Read(reader, binary.BigEndian, &ext); err != nil { return nil, fmt.Errorf("ws read: extended len 16: %w", err) } payloadLen = int64(ext) case 127: var ext uint64 if err := binary.Read(reader, binary.BigEndian, &ext); err != nil { return nil, fmt.Errorf("ws read: extended len 64: %w", err) } payloadLen = int64(ext) } // Leer mascara si aplica (servidor->cliente normalmente no tiene mascara) var mask [4]byte if masked { if _, err := io.ReadFull(reader, mask[:]); err != nil { return nil, fmt.Errorf("ws read: mask: %w", err) } } // Leer payload payload := make([]byte, payloadLen) if _, err := io.ReadFull(reader, payload); err != nil { return nil, fmt.Errorf("ws read: payload: %w", err) } // Aplicar mascara si hay if masked { for i := range payload { payload[i] ^= mask[i%4] } } return payload, nil } // wsWriteMessage escribe un frame WebSocket enmascarado (cliente->servidor requiere mascara). func wsWriteMessage(conn net.Conn, data []byte) error { // Generar mascara aleatoria var mask [4]byte if _, err := rand.Read(mask[:]); err != nil { return fmt.Errorf("ws write: generar mascara: %w", err) } // Construir frame payloadLen := len(data) var header []byte header = append(header, 0x81) // FIN + opcode text if payloadLen < 126 { header = append(header, byte(payloadLen)|0x80) // masked } else if payloadLen < 65536 { header = append(header, 126|0x80) header = append(header, byte(payloadLen>>8), byte(payloadLen)) } else { header = append(header, 127|0x80) b := make([]byte, 8) binary.BigEndian.PutUint64(b, uint64(payloadLen)) header = append(header, b...) } header = append(header, mask[:]...) // Aplicar mascara al payload masked := make([]byte, payloadLen) for i, b := range data { masked[i] = b ^ mask[i%4] } frame := append(header, masked...) if _, err := conn.Write(frame); err != nil { return fmt.Errorf("ws write: %w", err) } return nil } // sendCDP envia un comando CDP y espera la respuesta con el mismo ID. func (c *CDPConn) sendCDP(method string, params map[string]any) (map[string]any, error) { id := c.nextID.Add(1) req := cdpRequest{ID: id, Method: method, Params: params} data, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("cdp send: marshal: %w", err) } // Registrar canal para la respuesta ch := make(chan cdpResponse, 1) c.pendMu.Lock() c.pending[id] = ch c.pendMu.Unlock() // Enviar frame WebSocket c.mu.Lock() err = wsWriteMessage(c.conn, data) c.mu.Unlock() if err != nil { c.pendMu.Lock() delete(c.pending, id) c.pendMu.Unlock() return nil, fmt.Errorf("cdp send %s: %w", method, err) } // Esperar respuesta resp := <-ch if resp.Error != nil { return nil, fmt.Errorf("cdp %s: error %d: %s", method, resp.Error.Code, resp.Error.Message) } return resp.Result, nil } // readLoop lee mensajes del WebSocket y los enruta a los canales pendientes. // Debe ejecutarse en una goroutine. func (c *CDPConn) readLoop() { for { data, err := wsReadMessage(c.reader) if err != nil { // Conexion cerrada o error — notificar a todos los pendientes c.pendMu.Lock() for _, ch := range c.pending { ch <- cdpResponse{Error: &cdpError{Message: err.Error()}} } c.pending = map[int64]chan cdpResponse{} c.pendMu.Unlock() return } var resp cdpResponse if err := json.Unmarshal(data, &resp); err != nil { continue } // Si tiene ID, es respuesta a un comando if resp.ID > 0 { c.pendMu.Lock() ch, ok := c.pending[resp.ID] if ok { delete(c.pending, resp.ID) } c.pendMu.Unlock() if ok { ch <- resp } } // Si no tiene ID, es un evento CDP — por ahora los ignoramos // Las funciones que necesiten eventos usan polling o envian el comando y esperan } } // cdpGetWSURL obtiene el webSocketDebuggerUrl del endpoint HTTP de CDP. func cdpGetWSURL(port int) (string, error) { resp, err := http.Get(fmt.Sprintf("http://localhost:%d/json/version", port)) if err != nil { return "", fmt.Errorf("cdp version: %w", err) } defer resp.Body.Close() var info cdpVersionResponse if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return "", fmt.Errorf("cdp version: decode: %w", err) } if info.WebSocketDebuggerURL == "" { return "", fmt.Errorf("cdp version: webSocketDebuggerUrl vacio") } return info.WebSocketDebuggerURL, nil }