Files
Developer 6e31ad38de feat: gestión de extensiones de Chrome
Implementa sistema para cargar y gestionar extensiones.

Incluye:
- Cargar extensiones desde carpetas o archivos .crx
- Config.Extensions para especificar al lanzar
- buildExtensionFlags() integrado en Launch()
- Extensiones predefinidas (uBlock, Tampermonkey)
- ListLocalExtensions() y GetExtensionPath()

Flags utilizadas: --load-extension, --disable-extensions-except

Archivo: pkg/browser/extensions.go, pkg/browser/browser.go
2026-03-25 00:48:01 +01:00

381 lines
9.0 KiB
Go

package browser
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"navegator/pkg/cdp"
"navegator/pkg/stealth"
)
// Browser representa una instancia de Chrome/Chromium.
type Browser struct {
cmd *exec.Cmd
cdpClient *cdp.Client
config *Config
profilePath string
debugURL string
ctx context.Context
cancel context.CancelFunc
targetID string
recorder *Recorder
}
// Config contiene la configuración para lanzar el navegador.
type Config struct {
// ExecutablePath es la ruta al ejecutable de Chrome/Chromium
// Si está vacío, se buscará automáticamente
ExecutablePath string
// ProfileName es el nombre del perfil a usar/crear
ProfileName string
// ProfilesBaseDir es el directorio base donde se guardan los perfiles
// Por defecto: ~/.navegator/profiles
ProfilesBaseDir string
// StealthFlags son las configuraciones stealth
StealthFlags *stealth.StealthFlags
// Extensions son las extensiones a cargar
Extensions []*ExtensionConfig
// DisableOtherExts deshabilita todas las extensiones excepto las especificadas
DisableOtherExts bool
// Timeout para iniciar el navegador
StartTimeout time.Duration
// Env variables de entorno adicionales
Env []string
}
// DefaultConfig retorna una configuración por defecto.
func DefaultConfig() *Config {
homeDir, _ := os.UserHomeDir()
defaultProfilesDir := filepath.Join(homeDir, ".navegator", "profiles")
return &Config{
ProfilesBaseDir: defaultProfilesDir,
ProfileName: "default",
StealthFlags: stealth.DefaultStealthFlags(),
StartTimeout: 30 * time.Second,
}
}
// Launch inicia una nueva instancia del navegador.
func Launch(ctx context.Context, config *Config) (*Browser, error) {
if config == nil {
config = DefaultConfig()
}
// Buscar ejecutable de Chrome si no está especificado
if config.ExecutablePath == "" {
exe, err := findChrome()
if err != nil {
return nil, fmt.Errorf("failed to find Chrome executable: %w", err)
}
config.ExecutablePath = exe
}
// Crear directorio de perfil
profilePath := filepath.Join(config.ProfilesBaseDir, config.ProfileName)
if err := os.MkdirAll(profilePath, 0755); err != nil {
return nil, fmt.Errorf("failed to create profile directory: %w", err)
}
// Configurar flags stealth con el profilePath
config.StealthFlags.UserDataDir = profilePath
config.StealthFlags.ProfileName = "Default"
// Construir flags
flags := config.StealthFlags.Build()
// Agregar flags de extensiones
extFlags := config.buildExtensionFlags()
flags = append(flags, extFlags...)
// Crear comando
cmd := exec.CommandContext(ctx, config.ExecutablePath, flags...)
cmd.Env = append(os.Environ(), config.Env...)
// Iniciar Chrome
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start Chrome: %w", err)
}
browserCtx, cancel := context.WithCancel(ctx)
b := &Browser{
cmd: cmd,
config: config,
profilePath: profilePath,
ctx: browserCtx,
cancel: cancel,
}
// Esperar a que Chrome esté listo
if err := b.waitForChrome(config.StartTimeout); err != nil {
b.Close()
return nil, err
}
// Conectar CDP
if err := b.connectCDP(); err != nil {
b.Close()
return nil, err
}
// Inyectar script anti-detección
if err := b.injectAntiDetection(); err != nil {
// No es crítico, continuar
fmt.Fprintf(os.Stderr, "Warning: failed to inject anti-detection script: %v\n", err)
}
return b, nil
}
// waitForChrome espera a que Chrome esté listo y escuchando CDP.
func (b *Browser) waitForChrome(timeout time.Duration) error {
// Leer el archivo DevToolsActivePort para obtener el puerto
devToolsFile := filepath.Join(b.profilePath, "DevToolsActivePort")
ctx, cancel := context.WithTimeout(b.ctx, timeout)
defer cancel()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return errors.New("timeout waiting for Chrome to start")
case <-ticker.C:
data, err := os.ReadFile(devToolsFile)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
if len(lines) < 1 {
continue
}
port := strings.TrimSpace(lines[0])
if port == "" {
continue
}
b.debugURL = "http://127.0.0.1:" + port
return nil
}
}
}
// connectCDP conecta al cliente CDP.
func (b *Browser) connectCDP() error {
wsURL, err := cdp.GetWebSocketURL(b.ctx, b.debugURL)
if err != nil {
return fmt.Errorf("failed to get WebSocket URL: %w", err)
}
client, err := cdp.NewClient(b.ctx, wsURL)
if err != nil {
return fmt.Errorf("failed to create CDP client: %w", err)
}
b.cdpClient = client
// Habilitar dominios necesarios
if err := b.enableDomains(); err != nil {
return fmt.Errorf("failed to enable CDP domains: %w", err)
}
// Obtener target ID
if err := b.getTargetID(); err != nil {
return fmt.Errorf("failed to get target ID: %w", err)
}
return nil
}
// enableDomains habilita los dominios CDP necesarios.
func (b *Browser) enableDomains() error {
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
// Solo algunos dominios tienen método .enable
domains := []string{
"Network",
"Runtime",
"DOM",
}
for _, domain := range domains {
if err := b.cdpClient.Execute(ctx, domain+".enable", nil, nil); err != nil {
return fmt.Errorf("failed to enable %s domain: %w", domain, err)
}
}
// Page.enable no existe, Page se activa automáticamente
// Storage.enable no existe, Storage funciona directamente
// Fetch.enable se llama manualmente cuando se necesita interceptación
return nil
}
// getTargetID obtiene el ID del target principal.
func (b *Browser) getTargetID() error {
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
var result struct {
TargetInfos []struct {
TargetID string `json:"targetId"`
Type string `json:"type"`
} `json:"targetInfos"`
}
if err := b.cdpClient.Execute(ctx, "Target.getTargets", nil, &result); err != nil {
return err
}
for _, info := range result.TargetInfos {
if info.Type == "page" {
b.targetID = info.TargetID
return nil
}
}
return errors.New("no page target found")
}
// injectAntiDetection inyecta el script anti-detección en todas las páginas.
func (b *Browser) injectAntiDetection() error {
script := stealth.GetAntiDetectionScript()
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
params := map[string]interface{}{
"source": script,
}
return b.cdpClient.Execute(ctx, "Page.addScriptToEvaluateOnNewDocument", params, nil)
}
// Client retorna el cliente CDP subyacente.
func (b *Browser) Client() *cdp.Client {
return b.cdpClient
}
// ProfilePath retorna la ruta del perfil usado.
func (b *Browser) ProfilePath() string {
return b.profilePath
}
// DebugURL retorna la URL de debugging de Chrome.
func (b *Browser) DebugURL() string {
return b.debugURL
}
// TargetID retorna el ID del target principal.
func (b *Browser) TargetID() string {
return b.targetID
}
// Close cierra el navegador y limpia recursos.
func (b *Browser) Close() error {
b.cancel()
if b.recorder != nil {
b.recorder.Close()
}
if b.cdpClient != nil {
b.cdpClient.Close()
}
if b.cmd != nil && b.cmd.Process != nil {
b.cmd.Process.Kill()
b.cmd.Wait()
}
return nil
}
// StartRecording inicia el registro de acciones en un archivo.
func (b *Browser) StartRecording(filepath string) error {
recorder, err := NewRecorder(filepath)
if err != nil {
return err
}
b.recorder = recorder
return nil
}
// StopRecording detiene el registro de acciones.
func (b *Browser) StopRecording() error {
if b.recorder != nil {
return b.recorder.Close()
}
return nil
}
// AddComment agrega un comentario al log de recording.
func (b *Browser) AddComment(comment string) {
if b.recorder != nil {
b.recorder.AddComment(comment)
}
}
// findChrome busca el ejecutable de Chrome en las ubicaciones comunes.
func findChrome() (string, error) {
var candidates []string
switch runtime.GOOS {
case "darwin":
candidates = []string{
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
}
case "windows":
candidates = []string{
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Users\\" + os.Getenv("USERNAME") + "\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
}
default: // linux
candidates = []string{
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
}
}
// Verificar cada candidato
for _, path := range candidates {
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
// Intentar buscar en PATH
for _, name := range []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser"} {
if path, err := exec.LookPath(name); err == nil {
return path, nil
}
}
return "", errors.New("Chrome/Chromium executable not found")
}