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:
+151
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"navegator/pkg/browser"
|
||||
)
|
||||
|
||||
// Resultado representa un resultado de búsqueda
|
||||
type Resultado struct {
|
||||
Titulo string `json:"titulo"`
|
||||
URL string `json:"url"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Definir flags/parámetros
|
||||
query := flag.String("q", "", "Consulta de búsqueda (requerido)")
|
||||
maxResults := flag.Int("n", 10, "Número máximo de resultados (default: 10)")
|
||||
headless := flag.Bool("headless", true, "Modo headless (default: true)")
|
||||
outputJSON := flag.String("output", "", "Guardar resultados en archivo JSON")
|
||||
profileName := flag.String("profile", "search-bot", "Nombre del perfil a usar")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validar que se proporcionó la consulta
|
||||
if *query == "" {
|
||||
fmt.Println("Error: debes proporcionar una consulta con -q")
|
||||
fmt.Println("\nEjemplo:")
|
||||
fmt.Println(" ./buscar -q \"golang tutorial\" -n 20")
|
||||
fmt.Println("\nOpciones:")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Configurar navegador
|
||||
currentDir, _ := os.Getwd()
|
||||
profilesDir := filepath.Join(currentDir, "perfiles")
|
||||
|
||||
config := browser.DefaultConfig()
|
||||
config.ProfilesBaseDir = profilesDir
|
||||
config.ProfileName = *profileName
|
||||
config.StealthFlags.Headless = *headless
|
||||
config.StealthFlags.WindowSize = [2]int{1280, 720}
|
||||
|
||||
log.Printf("🔍 Buscando: %s", *query)
|
||||
log.Printf("📊 Máximo de resultados: %d", *maxResults)
|
||||
log.Printf("👤 Usando perfil: %s", *profileName)
|
||||
|
||||
// Lanzar navegador
|
||||
b, err := browser.Launch(ctx, config)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al lanzar navegador: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar a DuckDuckGo (más amigable para bots que Google)
|
||||
searchURL := fmt.Sprintf("https://duckduckgo.com/?q=%s", *query)
|
||||
log.Println("🌐 Navegando a DuckDuckGo...")
|
||||
|
||||
if err := b.Navigate(ctx, searchURL, nil); err != nil {
|
||||
log.Fatalf("❌ Error al navegar: %v", err)
|
||||
}
|
||||
|
||||
// Esperar a que carguen los resultados
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
log.Println("📥 Extrayendo resultados...")
|
||||
|
||||
// Script para extraer resultados
|
||||
extractScript := fmt.Sprintf(`
|
||||
(() => {
|
||||
const results = [];
|
||||
const maxResults = %d;
|
||||
|
||||
// DuckDuckGo usa article con data-testid="result"
|
||||
const items = document.querySelectorAll('article[data-testid="result"]');
|
||||
|
||||
for (let i = 0; i < Math.min(items.length, maxResults); i++) {
|
||||
const item = items[i];
|
||||
|
||||
// Título
|
||||
const titleEl = item.querySelector('h2 a');
|
||||
const titulo = titleEl ? titleEl.textContent : '';
|
||||
const url = titleEl ? titleEl.href : '';
|
||||
|
||||
// Descripción
|
||||
const descEl = item.querySelector('[data-result="snippet"]');
|
||||
const descripcion = descEl ? descEl.textContent : '';
|
||||
|
||||
if (titulo && url) {
|
||||
results.push({
|
||||
titulo: titulo.trim(),
|
||||
url: url,
|
||||
descripcion: descripcion.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
})()
|
||||
`, *maxResults)
|
||||
|
||||
result, err := b.Evaluate(ctx, extractScript)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al extraer resultados: %v", err)
|
||||
}
|
||||
|
||||
// Parsear resultados
|
||||
resultadosJSON, err := json.Marshal(result.Value)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al parsear resultados: %v", err)
|
||||
}
|
||||
|
||||
var resultados []Resultado
|
||||
if err := json.Unmarshal(resultadosJSON, &resultados); err != nil {
|
||||
log.Fatalf("❌ Error al deserializar: %v", err)
|
||||
}
|
||||
|
||||
// Mostrar resultados
|
||||
log.Printf("\n✅ Encontrados %d resultados:\n", len(resultados))
|
||||
|
||||
for i, r := range resultados {
|
||||
fmt.Printf("\n%d. %s\n", i+1, r.Titulo)
|
||||
fmt.Printf(" 🔗 %s\n", r.URL)
|
||||
if r.Descripcion != "" {
|
||||
fmt.Printf(" 📝 %s\n", r.Descripcion)
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar en JSON si se especificó
|
||||
if *outputJSON != "" {
|
||||
data, _ := json.MarshalIndent(resultados, "", " ")
|
||||
if err := os.WriteFile(*outputJSON, data, 0644); err != nil {
|
||||
log.Printf("⚠️ Error al guardar JSON: %v", err)
|
||||
} else {
|
||||
log.Printf("\n💾 Resultados guardados en: %s", *outputJSON)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("\n✨ Búsqueda completada!")
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"navegator/pkg/browser"
|
||||
)
|
||||
|
||||
// Resultado representa un resultado de búsqueda
|
||||
type Resultado struct {
|
||||
Titulo string `json:"titulo"`
|
||||
URL string `json:"url"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Definir flags/parámetros
|
||||
query := flag.String("q", "", "Consulta de búsqueda (requerido)")
|
||||
maxResults := flag.Int("n", 10, "Número máximo de resultados (default: 10)")
|
||||
headless := flag.Bool("headless", true, "Modo headless (default: true)")
|
||||
outputJSON := flag.String("output", "", "Guardar resultados en archivo JSON")
|
||||
profileName := flag.String("profile", "search-bot", "Nombre del perfil a usar")
|
||||
|
||||
// NUEVO: Directorio de perfiles compartido
|
||||
profilesDir := flag.String("profiles-dir", "", "Directorio de perfiles (default: ~/.navegator/profiles)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validar que se proporcionó la consulta
|
||||
if *query == "" {
|
||||
fmt.Println("Error: debes proporcionar una consulta con -q")
|
||||
fmt.Println("\nEjemplo:")
|
||||
fmt.Println(" ./buscar -q \"golang tutorial\" -n 20")
|
||||
fmt.Println("\nOpciones:")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Determinar directorio de perfiles
|
||||
var perfilesPath string
|
||||
if *profilesDir != "" {
|
||||
perfilesPath = *profilesDir
|
||||
} else {
|
||||
// Default: ~/.navegator/profiles
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al obtener home dir: %v", err)
|
||||
}
|
||||
perfilesPath = filepath.Join(homeDir, ".navegator", "profiles")
|
||||
}
|
||||
|
||||
// Crear directorio si no existe
|
||||
if err := os.MkdirAll(perfilesPath, 0755); err != nil {
|
||||
log.Fatalf("❌ Error al crear directorio de perfiles: %v", err)
|
||||
}
|
||||
|
||||
config := browser.DefaultConfig()
|
||||
config.ProfilesBaseDir = perfilesPath
|
||||
config.ProfileName = *profileName
|
||||
config.StealthFlags.Headless = *headless
|
||||
config.StealthFlags.WindowSize = [2]int{1280, 720}
|
||||
|
||||
log.Printf("🔍 Buscando: %s", *query)
|
||||
log.Printf("📊 Máximo de resultados: %d", *maxResults)
|
||||
log.Printf("👤 Usando perfil: %s", *profileName)
|
||||
log.Printf("📂 Perfiles en: %s", perfilesPath)
|
||||
|
||||
// Lanzar navegador
|
||||
b, err := browser.Launch(ctx, config)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al lanzar navegador: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar a DuckDuckGo
|
||||
searchURL := fmt.Sprintf("https://duckduckgo.com/?q=%s", *query)
|
||||
log.Println("🌐 Navegando a DuckDuckGo...")
|
||||
|
||||
if err := b.Navigate(ctx, searchURL, nil); err != nil {
|
||||
log.Fatalf("❌ Error al navegar: %v", err)
|
||||
}
|
||||
|
||||
// Esperar a que carguen los resultados
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
log.Println("📥 Extrayendo resultados...")
|
||||
|
||||
// Script para extraer resultados
|
||||
extractScript := fmt.Sprintf(`
|
||||
(() => {
|
||||
const results = [];
|
||||
const maxResults = %d;
|
||||
|
||||
const items = document.querySelectorAll('article[data-testid="result"]');
|
||||
|
||||
for (let i = 0; i < Math.min(items.length, maxResults); i++) {
|
||||
const item = items[i];
|
||||
|
||||
const titleEl = item.querySelector('h2 a');
|
||||
const titulo = titleEl ? titleEl.textContent : '';
|
||||
const url = titleEl ? titleEl.href : '';
|
||||
|
||||
const descEl = item.querySelector('[data-result="snippet"]');
|
||||
const descripcion = descEl ? descEl.textContent : '';
|
||||
|
||||
if (titulo && url) {
|
||||
results.push({
|
||||
titulo: titulo.trim(),
|
||||
url: url,
|
||||
descripcion: descripcion.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
})()
|
||||
`, *maxResults)
|
||||
|
||||
result, err := b.Evaluate(ctx, extractScript)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al extraer resultados: %v", err)
|
||||
}
|
||||
|
||||
// Parsear resultados
|
||||
resultadosJSON, err := json.Marshal(result.Value)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al parsear resultados: %v", err)
|
||||
}
|
||||
|
||||
var resultados []Resultado
|
||||
if err := json.Unmarshal(resultadosJSON, &resultados); err != nil {
|
||||
log.Fatalf("❌ Error al deserializar: %v", err)
|
||||
}
|
||||
|
||||
// Mostrar resultados
|
||||
log.Printf("\n✅ Encontrados %d resultados:\n", len(resultados))
|
||||
|
||||
for i, r := range resultados {
|
||||
fmt.Printf("\n%d. %s\n", i+1, r.Titulo)
|
||||
fmt.Printf(" 🔗 %s\n", r.URL)
|
||||
if r.Descripcion != "" {
|
||||
fmt.Printf(" 📝 %s\n", r.Descripcion)
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar en JSON si se especificó
|
||||
if *outputJSON != "" {
|
||||
data, _ := json.MarshalIndent(resultados, "", " ")
|
||||
if err := os.WriteFile(*outputJSON, data, 0644); err != nil {
|
||||
log.Printf("⚠️ Error al guardar JSON: %v", err)
|
||||
} else {
|
||||
log.Printf("\n💾 Resultados guardados en: %s", *outputJSON)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("\n✨ Búsqueda completada!")
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"navegator/pkg/browser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parámetros
|
||||
url := flag.String("url", "", "URL a visitar (requerido)")
|
||||
profile := flag.String("profile", "user-default", "Perfil de navegador a usar")
|
||||
headless := flag.Bool("headless", false, "Modo headless (default: false para ver la navegación)")
|
||||
duration := flag.Int("duration", 10, "Segundos que mantener abierto el navegador")
|
||||
click := flag.String("click", "", "Selector CSS para hacer click (opcional)")
|
||||
type_ := flag.String("type", "", "Selector CSS donde escribir (opcional)")
|
||||
text := flag.String("text", "", "Texto a escribir (requiere -type)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *url == "" {
|
||||
fmt.Println("Error: debes proporcionar una URL con -url")
|
||||
fmt.Println("\nEjemplo básico:")
|
||||
fmt.Println(" ./navegar -url https://example.com -profile usuario1")
|
||||
fmt.Println("\nEjemplo con interacción:")
|
||||
fmt.Println(" ./navegar -url https://example.com -click 'a[href]' -duration 30")
|
||||
fmt.Println("\nEjemplo con formulario:")
|
||||
fmt.Println(" ./navegar -url https://httpbin.org/forms/post -type 'input[name=\"custname\"]' -text 'Juan Pérez'")
|
||||
fmt.Println("\nOpciones:")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Configurar navegador
|
||||
currentDir, _ := os.Getwd()
|
||||
profilesDir := filepath.Join(currentDir, "perfiles")
|
||||
|
||||
config := browser.DefaultConfig()
|
||||
config.ProfilesBaseDir = profilesDir
|
||||
config.ProfileName = *profile
|
||||
config.StealthFlags.Headless = *headless
|
||||
config.StealthFlags.WindowSize = [2]int{1280, 720}
|
||||
|
||||
log.Printf("🌐 Navegando: %s", *url)
|
||||
log.Printf("👤 Perfil: %s", *profile)
|
||||
log.Printf("⏱️ Duración: %d segundos", *duration)
|
||||
|
||||
// Lanzar navegador
|
||||
b, err := browser.Launch(ctx, config)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Iniciar recording
|
||||
recordingFile := filepath.Join(currentDir, fmt.Sprintf("recording_%s.log", *profile))
|
||||
if err := b.StartRecording(recordingFile); err != nil {
|
||||
log.Printf("⚠️ Recording desactivado: %v", err)
|
||||
} else {
|
||||
log.Printf("📝 Recording: %s", recordingFile)
|
||||
}
|
||||
|
||||
b.AddComment(fmt.Sprintf("=== Sesión de %s ===", *profile))
|
||||
|
||||
// Navegar
|
||||
opts := browser.DefaultNavigateOptions()
|
||||
opts.Timeout = 30 * time.Second
|
||||
|
||||
if err := b.Navigate(ctx, *url, opts); err != nil {
|
||||
log.Printf("⚠️ Advertencia: %v", err)
|
||||
} else {
|
||||
log.Println("✅ Página cargada")
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Click si se especificó
|
||||
if *click != "" {
|
||||
b.AddComment(fmt.Sprintf("Click en: %s", *click))
|
||||
log.Printf("🖱️ Haciendo click en: %s", *click)
|
||||
if err := b.Click(ctx, *click); err != nil {
|
||||
log.Printf("⚠️ Error al hacer click: %v", err)
|
||||
} else {
|
||||
log.Println("✅ Click realizado")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// Type si se especificó
|
||||
if *type_ != "" && *text != "" {
|
||||
b.AddComment(fmt.Sprintf("Escribiendo en: %s", *type_))
|
||||
log.Printf("⌨️ Escribiendo '%s' en: %s", *text, *type_)
|
||||
if err := b.Type(ctx, *type_, *text, nil); err != nil {
|
||||
log.Printf("⚠️ Error al escribir: %v", err)
|
||||
} else {
|
||||
log.Println("✅ Texto escrito")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener información de la página
|
||||
title, _ := b.Evaluate(ctx, "document.title")
|
||||
log.Printf("📄 Título: %v", title.Value)
|
||||
|
||||
currentURL, _ := b.Evaluate(ctx, "window.location.href")
|
||||
log.Printf("🔗 URL actual: %v", currentURL.Value)
|
||||
|
||||
// Mantener navegador abierto
|
||||
log.Printf("\n⏳ Manteniendo navegador abierto por %d segundos...", *duration)
|
||||
time.Sleep(time.Duration(*duration) * time.Second)
|
||||
|
||||
b.AddComment("Sesión finalizada")
|
||||
log.Println("✨ Completado!")
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"navegator/pkg/browser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parámetros
|
||||
url := flag.String("url", "", "URL a capturar (requerido)")
|
||||
output := flag.String("o", "screenshot.png", "Archivo de salida")
|
||||
profile := flag.String("profile", "screenshot-bot", "Perfil de navegador a usar")
|
||||
headless := flag.Bool("headless", true, "Modo headless")
|
||||
fullPage := flag.Bool("full", false, "Captura página completa")
|
||||
width := flag.Int("width", 1280, "Ancho de ventana")
|
||||
height := flag.Int("height", 720, "Alto de ventana")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *url == "" {
|
||||
fmt.Println("Error: debes proporcionar una URL con -url")
|
||||
fmt.Println("\nEjemplo:")
|
||||
fmt.Println(" ./screenshot -url https://example.com -o captura.png")
|
||||
fmt.Println("\nOpciones:")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Configurar navegador
|
||||
currentDir, _ := os.Getwd()
|
||||
profilesDir := filepath.Join(currentDir, "perfiles")
|
||||
|
||||
config := browser.DefaultConfig()
|
||||
config.ProfilesBaseDir = profilesDir
|
||||
config.ProfileName = *profile
|
||||
config.StealthFlags.Headless = *headless
|
||||
config.StealthFlags.WindowSize = [2]int{*width, *height}
|
||||
|
||||
log.Printf("📸 Capturando: %s", *url)
|
||||
log.Printf("👤 Usando perfil: %s", *profile)
|
||||
|
||||
// Lanzar navegador
|
||||
b, err := browser.Launch(ctx, config)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar
|
||||
opts := browser.DefaultNavigateOptions()
|
||||
opts.Timeout = 15 * 1000000000 // 15 segundos
|
||||
|
||||
if err := b.Navigate(ctx, *url, opts); err != nil {
|
||||
log.Printf("⚠️ Timeout en navegación, pero continuando...")
|
||||
}
|
||||
|
||||
// Tomar screenshot
|
||||
log.Println("📷 Capturando pantalla...")
|
||||
screenshot, err := b.Screenshot(ctx, *fullPage)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Error al capturar: %v", err)
|
||||
}
|
||||
|
||||
// Guardar
|
||||
if err := os.WriteFile(*output, screenshot, 0644); err != nil {
|
||||
log.Fatalf("❌ Error al guardar: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ Screenshot guardado: %s (%d bytes)", *output, len(screenshot))
|
||||
}
|
||||
Reference in New Issue
Block a user