feat: gestión avanzada de cookies
Implementa sistema completo de import/export y gestión de cookies. Incluye: - GetAllCookies() y FilterCookies() para búsqueda - ExportCookiesToFile() / ImportCookiesFromFile() en JSON y Netscape - DeleteCookiesByDomain() para limpieza - ListProfiles() para gestión de perfiles - Comando CLI cookies.go con subcomandos Formatos soportados: JSON estándar y Netscape cookies.txt Archivo: pkg/browser/profile_cookies.go, cmd/cookies.go
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"navegator/pkg/browser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Subcomandos
|
||||
listCmd := flag.NewFlagSet("list", flag.ExitOnError)
|
||||
listURL := listCmd.String("url", "", "URL to navigate before listing cookies")
|
||||
listDomain := listCmd.String("domain", "", "Filter by domain")
|
||||
|
||||
exportCmd := flag.NewFlagSet("export", flag.ExitOnError)
|
||||
exportURL := exportCmd.String("url", "", "URL to navigate before exporting")
|
||||
exportFile := exportCmd.String("output", "cookies.json", "Output file")
|
||||
exportFormat := exportCmd.String("format", "json", "Format: json or netscape")
|
||||
|
||||
importCmd := flag.NewFlagSet("import", flag.ExitOnError)
|
||||
importURL := importCmd.String("url", "", "URL to navigate before importing")
|
||||
importFile := importCmd.String("input", "", "Input file (required)")
|
||||
importFormat := importCmd.String("format", "json", "Format: json or netscape")
|
||||
|
||||
deleteCmd := flag.NewFlagSet("delete", flag.ExitOnError)
|
||||
deleteURL := deleteCmd.String("url", "", "URL to navigate before deleting")
|
||||
deleteDomain := deleteCmd.String("domain", "", "Domain to delete cookies from (required)")
|
||||
|
||||
profilesCmd := flag.NewFlagSet("profiles", flag.ExitOnError)
|
||||
|
||||
if len(flag.Args()) < 1 {
|
||||
fmt.Println("Usage: cookies <command> [options]")
|
||||
fmt.Println("\nCommands:")
|
||||
fmt.Println(" list List cookies")
|
||||
fmt.Println(" export Export cookies to file")
|
||||
fmt.Println(" import Import cookies from file")
|
||||
fmt.Println(" delete Delete cookies by domain")
|
||||
fmt.Println(" profiles List available profiles")
|
||||
return
|
||||
}
|
||||
|
||||
command := flag.Args()[0]
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch command {
|
||||
case "list":
|
||||
listCmd.Parse(flag.Args()[1:])
|
||||
listCookies(ctx, *listURL, *listDomain)
|
||||
|
||||
case "export":
|
||||
exportCmd.Parse(flag.Args()[1:])
|
||||
exportCookies(ctx, *exportURL, *exportFile, *exportFormat)
|
||||
|
||||
case "import":
|
||||
importCmd.Parse(flag.Args()[1:])
|
||||
if *importFile == "" {
|
||||
log.Fatal("Error: -input is required")
|
||||
}
|
||||
importCookies(ctx, *importURL, *importFile, *importFormat)
|
||||
|
||||
case "delete":
|
||||
deleteCmd.Parse(flag.Args()[1:])
|
||||
if *deleteDomain == "" {
|
||||
log.Fatal("Error: -domain is required")
|
||||
}
|
||||
deleteCookies(ctx, *deleteURL, *deleteDomain)
|
||||
|
||||
case "profiles":
|
||||
profilesCmd.Parse(flag.Args()[1:])
|
||||
listProfiles()
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
func listCookies(ctx context.Context, url, domain string) {
|
||||
b := launchBrowser(ctx, url)
|
||||
defer b.Close()
|
||||
|
||||
var cookies []*browser.Cookie
|
||||
var err error
|
||||
|
||||
if domain != "" {
|
||||
cookies, err = b.FilterCookies(ctx, browser.CookieFilter{Domain: domain})
|
||||
} else {
|
||||
cookies, err = b.GetAllCookies(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting cookies: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Cookies (%d) ===\n\n", len(cookies))
|
||||
for i, cookie := range cookies {
|
||||
fmt.Printf("%d. %s = %s\n", i+1, cookie.Name, cookie.Value)
|
||||
fmt.Printf(" Domain: %s\n", cookie.Domain)
|
||||
fmt.Printf(" Path: %s\n", cookie.Path)
|
||||
fmt.Printf(" Secure: %v, HttpOnly: %v\n", cookie.Secure, cookie.HTTPOnly)
|
||||
if cookie.SameSite != "" {
|
||||
fmt.Printf(" SameSite: %s\n", cookie.SameSite)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func exportCookies(ctx context.Context, url, output, format string) {
|
||||
b := launchBrowser(ctx, url)
|
||||
defer b.Close()
|
||||
|
||||
var cookieFormat browser.CookieFormat
|
||||
switch format {
|
||||
case "json":
|
||||
cookieFormat = browser.CookieFormatJSON
|
||||
case "netscape":
|
||||
cookieFormat = browser.CookieFormatNetscape
|
||||
default:
|
||||
log.Fatalf("Unknown format: %s", format)
|
||||
}
|
||||
|
||||
log.Printf("Exporting cookies to %s...\n", output)
|
||||
if err := b.ExportCookiesToFile(ctx, output, cookieFormat); err != nil {
|
||||
log.Fatalf("Error exporting cookies: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Cookies exported successfully to %s\n", output)
|
||||
}
|
||||
|
||||
func importCookies(ctx context.Context, url, input, format string) {
|
||||
b := launchBrowser(ctx, url)
|
||||
defer b.Close()
|
||||
|
||||
var cookieFormat browser.CookieFormat
|
||||
switch format {
|
||||
case "json":
|
||||
cookieFormat = browser.CookieFormatJSON
|
||||
case "netscape":
|
||||
cookieFormat = browser.CookieFormatNetscape
|
||||
default:
|
||||
log.Fatalf("Unknown format: %s", format)
|
||||
}
|
||||
|
||||
log.Printf("Importing cookies from %s...\n", input)
|
||||
if err := b.ImportCookiesFromFile(ctx, input, cookieFormat); err != nil {
|
||||
log.Fatalf("Error importing cookies: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Cookies imported successfully")
|
||||
|
||||
// Verificar
|
||||
cookies, _ := b.GetAllCookies(ctx)
|
||||
log.Printf("Total cookies after import: %d\n", len(cookies))
|
||||
}
|
||||
|
||||
func deleteCookies(ctx context.Context, url, domain string) {
|
||||
b := launchBrowser(ctx, url)
|
||||
defer b.Close()
|
||||
|
||||
log.Printf("Deleting cookies for domain %s...\n", domain)
|
||||
if err := b.DeleteCookiesByDomain(ctx, domain); err != nil {
|
||||
log.Fatalf("Error deleting cookies: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Cookies deleted successfully")
|
||||
}
|
||||
|
||||
func listProfiles() {
|
||||
profiles, err := browser.ListProfiles()
|
||||
if err != nil {
|
||||
log.Fatalf("Error listing profiles: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Available Profiles (%d) ===\n\n", len(profiles))
|
||||
for i, profile := range profiles {
|
||||
fmt.Printf("%d. %s\n", i+1, profile.Name)
|
||||
fmt.Printf(" Path: %s\n", profile.Path)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func launchBrowser(ctx context.Context, url string) *browser.Browser {
|
||||
config := browser.DefaultConfig()
|
||||
config.ProfileName = "cookie-manager"
|
||||
config.StealthFlags.Headless = true
|
||||
|
||||
log.Println("Launching browser...")
|
||||
b, err := browser.Launch(ctx, config)
|
||||
if err != nil {
|
||||
log.Fatalf("Error launching browser: %v", err)
|
||||
}
|
||||
|
||||
if url != "" {
|
||||
log.Printf("Navigating to %s...\n", url)
|
||||
opts := browser.DefaultNavigateOptions()
|
||||
opts.WaitUntil = "load"
|
||||
|
||||
if err := b.Navigate(ctx, url, opts); err != nil {
|
||||
log.Printf("Warning: navigation error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CookieFormat formato de archivo de cookies
|
||||
type CookieFormat string
|
||||
|
||||
const (
|
||||
CookieFormatJSON CookieFormat = "json" // JSON estándar
|
||||
CookieFormatNetscape CookieFormat = "netscape" // cookies.txt formato Netscape
|
||||
)
|
||||
|
||||
// CookieFilter filtro para búsqueda de cookies
|
||||
type CookieFilter struct {
|
||||
Domain string // Filtrar por dominio (ej: ".example.com")
|
||||
Name string // Filtrar por nombre exacto
|
||||
Path string // Filtrar por path
|
||||
}
|
||||
|
||||
// GetAllCookies obtiene todas las cookies del navegador actual
|
||||
func (b *Browser) GetAllCookies(ctx context.Context) ([]*Cookie, error) {
|
||||
result, err := b.cdpClient.SendCommand(ctx, "Network.getAllCookies", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting all cookies: %w", err)
|
||||
}
|
||||
|
||||
var cookies []*Cookie
|
||||
if cookiesData, ok := result["cookies"].([]interface{}); ok {
|
||||
for _, cookieData := range cookiesData {
|
||||
if cookieMap, ok := cookieData.(map[string]interface{}); ok {
|
||||
cookie := parseCookieFromMap(cookieMap)
|
||||
cookies = append(cookies, cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// FilterCookies obtiene cookies que coinciden con filtros
|
||||
func (b *Browser) FilterCookies(ctx context.Context, filter CookieFilter) ([]*Cookie, error) {
|
||||
allCookies, err := b.GetAllCookies(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []*Cookie
|
||||
for _, cookie := range allCookies {
|
||||
match := true
|
||||
|
||||
if filter.Domain != "" && !strings.Contains(cookie.Domain, filter.Domain) {
|
||||
match = false
|
||||
}
|
||||
|
||||
if filter.Name != "" && cookie.Name != filter.Name {
|
||||
match = false
|
||||
}
|
||||
|
||||
if filter.Path != "" && cookie.Path != filter.Path {
|
||||
match = false
|
||||
}
|
||||
|
||||
if match {
|
||||
filtered = append(filtered, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// ExportCookiesToFile exporta cookies a archivo
|
||||
func (b *Browser) ExportCookiesToFile(ctx context.Context, filepath string, format CookieFormat) error {
|
||||
cookies, err := b.GetAllCookies(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var content string
|
||||
switch format {
|
||||
case CookieFormatJSON:
|
||||
content, err = cookiesToJSON(cookies)
|
||||
case CookieFormatNetscape:
|
||||
content = cookiesToNetscape(cookies)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error formatting cookies: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath, []byte(content), 0600); err != nil {
|
||||
return fmt.Errorf("error writing cookies file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportCookiesFromFile importa cookies desde archivo
|
||||
func (b *Browser) ImportCookiesFromFile(ctx context.Context, filepath string, format CookieFormat) error {
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading cookies file: %w", err)
|
||||
}
|
||||
|
||||
var cookies []*Cookie
|
||||
switch format {
|
||||
case CookieFormatJSON:
|
||||
cookies, err = cookiesFromJSON(data)
|
||||
case CookieFormatNetscape:
|
||||
cookies, err = cookiesFromNetscape(string(data))
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing cookies: %w", err)
|
||||
}
|
||||
|
||||
// Establecer cada cookie
|
||||
for _, cookie := range cookies {
|
||||
if err := b.SetCookie(ctx, cookie); err != nil {
|
||||
return fmt.Errorf("error setting cookie %s: %w", cookie.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCookiesByDomain elimina todas las cookies de un dominio específico
|
||||
func (b *Browser) DeleteCookiesByDomain(ctx context.Context, domain string) error {
|
||||
cookies, err := b.FilterCookies(ctx, CookieFilter{Domain: domain})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
params := map[string]interface{}{
|
||||
"name": cookie.Name,
|
||||
"domain": cookie.Domain,
|
||||
"path": cookie.Path,
|
||||
}
|
||||
|
||||
_, err := b.cdpClient.SendCommand(ctx, "Network.deleteCookies", params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting cookie %s: %w", cookie.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cookiesToJSON convierte cookies a formato JSON
|
||||
func cookiesToJSON(cookies []*Cookie) (string, error) {
|
||||
// Convertir a formato más simple para export
|
||||
type SimpleCookie struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Domain string `json:"domain"`
|
||||
Path string `json:"path"`
|
||||
Expires float64 `json:"expires,omitempty"`
|
||||
HTTPOnly bool `json:"httpOnly,omitempty"`
|
||||
Secure bool `json:"secure,omitempty"`
|
||||
SameSite string `json:"sameSite,omitempty"`
|
||||
}
|
||||
|
||||
simple := make([]SimpleCookie, len(cookies))
|
||||
for i, c := range cookies {
|
||||
simple[i] = SimpleCookie{
|
||||
Name: c.Name,
|
||||
Value: c.Value,
|
||||
Domain: c.Domain,
|
||||
Path: c.Path,
|
||||
Expires: c.Expires,
|
||||
HTTPOnly: c.HTTPOnly,
|
||||
Secure: c.Secure,
|
||||
SameSite: c.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := json.MarshalIndent(simple, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// cookiesFromJSON parsea cookies desde JSON
|
||||
func cookiesFromJSON(data []byte) ([]*Cookie, error) {
|
||||
type SimpleCookie struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Domain string `json:"domain"`
|
||||
Path string `json:"path"`
|
||||
Expires float64 `json:"expires"`
|
||||
HTTPOnly bool `json:"httpOnly"`
|
||||
Secure bool `json:"secure"`
|
||||
SameSite string `json:"sameSite"`
|
||||
}
|
||||
|
||||
var simple []SimpleCookie
|
||||
if err := json.Unmarshal(data, &simple); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookies := make([]*Cookie, len(simple))
|
||||
for i, s := range simple {
|
||||
cookies[i] = &Cookie{
|
||||
Name: s.Name,
|
||||
Value: s.Value,
|
||||
Domain: s.Domain,
|
||||
Path: s.Path,
|
||||
Expires: s.Expires,
|
||||
HTTPOnly: s.HTTPOnly,
|
||||
Secure: s.Secure,
|
||||
SameSite: s.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// cookiesToNetscape convierte cookies a formato Netscape cookies.txt
|
||||
func cookiesToNetscape(cookies []*Cookie) string {
|
||||
var lines []string
|
||||
lines = append(lines, "# Netscape HTTP Cookie File")
|
||||
lines = append(lines, "# This is a generated file. Do not edit.")
|
||||
lines = append(lines, "")
|
||||
|
||||
for _, c := range cookies {
|
||||
// Formato: domain flag path secure expiration name value
|
||||
domain := c.Domain
|
||||
if !strings.HasPrefix(domain, ".") {
|
||||
domain = "." + domain
|
||||
}
|
||||
|
||||
flag := "TRUE"
|
||||
secure := "FALSE"
|
||||
if c.Secure {
|
||||
secure = "TRUE"
|
||||
}
|
||||
|
||||
expiration := "0"
|
||||
if c.Expires > 0 {
|
||||
expiration = fmt.Sprintf("%.0f", c.Expires)
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%s",
|
||||
domain, flag, c.Path, secure, expiration, c.Name, c.Value)
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// cookiesFromNetscape parsea cookies desde formato Netscape
|
||||
func cookiesFromNetscape(data string) ([]*Cookie, error) {
|
||||
var cookies []*Cookie
|
||||
lines := strings.Split(data, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, "\t")
|
||||
if len(parts) != 7 {
|
||||
continue
|
||||
}
|
||||
|
||||
cookie := &Cookie{
|
||||
Domain: parts[0],
|
||||
Path: parts[2],
|
||||
Secure: parts[3] == "TRUE",
|
||||
Name: parts[5],
|
||||
Value: parts[6],
|
||||
}
|
||||
|
||||
// Parse expiration
|
||||
if parts[4] != "0" {
|
||||
fmt.Sscanf(parts[4], "%f", &cookie.Expires)
|
||||
}
|
||||
|
||||
cookies = append(cookies, cookie)
|
||||
}
|
||||
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// parseCookieFromMap parsea una cookie desde un map CDP
|
||||
func parseCookieFromMap(data map[string]interface{}) *Cookie {
|
||||
cookie := &Cookie{}
|
||||
|
||||
if name, ok := data["name"].(string); ok {
|
||||
cookie.Name = name
|
||||
}
|
||||
if value, ok := data["value"].(string); ok {
|
||||
cookie.Value = value
|
||||
}
|
||||
if domain, ok := data["domain"].(string); ok {
|
||||
cookie.Domain = domain
|
||||
}
|
||||
if path, ok := data["path"].(string); ok {
|
||||
cookie.Path = path
|
||||
}
|
||||
if expires, ok := data["expires"].(float64); ok {
|
||||
cookie.Expires = expires
|
||||
}
|
||||
if httpOnly, ok := data["httpOnly"].(bool); ok {
|
||||
cookie.HTTPOnly = httpOnly
|
||||
}
|
||||
if secure, ok := data["secure"].(bool); ok {
|
||||
cookie.Secure = secure
|
||||
}
|
||||
if sameSite, ok := data["sameSite"].(string); ok {
|
||||
cookie.SameSite = sameSite
|
||||
}
|
||||
|
||||
return cookie
|
||||
}
|
||||
|
||||
// Profile representa un perfil de navegador
|
||||
type Profile struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
// ListProfiles lista todos los perfiles disponibles en ~/.navegator/profiles
|
||||
func ListProfiles() ([]Profile, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profilesDir := filepath.Join(homeDir, ".navegator", "profiles")
|
||||
|
||||
entries, err := os.ReadDir(profilesDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []Profile{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var profiles []Profile
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
profiles = append(profiles, Profile{
|
||||
Name: entry.Name(),
|
||||
Path: filepath.Join(profilesDir, entry.Name()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
Reference in New Issue
Block a user