diff --git a/cmd/cookies.go b/cmd/cookies.go new file mode 100644 index 0000000..eb3c63b --- /dev/null +++ b/cmd/cookies.go @@ -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 [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 +} diff --git a/pkg/browser/profile_cookies.go b/pkg/browser/profile_cookies.go new file mode 100644 index 0000000..7679ffc --- /dev/null +++ b/pkg/browser/profile_cookies.go @@ -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 +}