package browser import ( "encoding/json" "fmt" "os" "strings" ) // CdpStorageState agrupa cookies, localStorage y sessionStorage capturados de una // pestaña activa. type CdpStorageState struct { Cookies []map[string]any `json:"cookies"` LocalStorage map[string]string `json:"localStorage"` SessionStorage map[string]string `json:"sessionStorage"` } // readWebStorage lee window. (localStorage|sessionStorage) como mapa. Si el // origen no permite acceso (about:blank, chrome://) devuelve un mapa vacío. func readWebStorage(c *CDPConn, store string) map[string]string { raw, err := CdpEvaluate(c, "JSON.stringify(Object.assign({}, window."+store+"))") if err != nil { return map[string]string{} } if raw == "" || raw == "undefined" || raw == "null" { return map[string]string{} } var m map[string]string if err := json.Unmarshal([]byte(raw), &m); err != nil { return map[string]string{} } return m } // cookieDomainMatchesHost indica si una cookie con `domain` aplica al `host` dado. // Cubre el caso de dominios con punto inicial (".example.com") y subdominios. func cookieDomainMatchesHost(domain, host string) bool { if domain == "" || host == "" { return false } d := strings.TrimPrefix(domain, ".") return host == d || strings.HasSuffix(host, "."+d) } // storageStateToMaps convierte []any (respuesta CDP) a []map[string]any. func storageStateToMaps(raw []any) []map[string]any { out := make([]map[string]any, 0, len(raw)) for _, item := range raw { if m, ok := item.(map[string]any); ok { out = append(out, m) } } return out } // CdpSaveStorageState captura cookies y localStorage de la pagina actual y los // escribe como JSON a outPath. Permite restaurar la sesion autenticada en // ejecuciones posteriores sin repetir el login. func CdpSaveStorageState(c *CDPConn, outPath string) error { if c == nil { return fmt.Errorf("cdp save storage state: conexion nula") } if outPath == "" { return fmt.Errorf("cdp save storage state: outPath vacio") } // Habilitar dominio Network para acceder a las cookies if _, err := c.sendCDP("Network.enable", nil); err != nil { return fmt.Errorf("cdp save storage state: Network.enable: %w", err) } // Obtener todas las cookies del perfil res, err := c.sendCDP("Network.getAllCookies", nil) if err != nil { return fmt.Errorf("cdp save storage state: getAllCookies: %w", err) } var cookies []map[string]any if rawCookies, ok := res["cookies"].([]any); ok { cookies = storageStateToMaps(rawCookies) } else { cookies = []map[string]any{} } // Filtrar al origen actual: Network.getAllCookies devuelve cookies de TODOS // los dominios del perfil. Para guardar "la sesión de ESTE sitio" solo // conservamos las que aplican al host cargado, evitando arrastrar cookies de // otros sitios visitados en la misma sesión del navegador. if host, herr := CdpEvaluate(c, "location.hostname"); herr == nil { host = strings.TrimSpace(host) if host != "" && host != "undefined" { filtered := make([]map[string]any, 0, len(cookies)) for _, ck := range cookies { dom, _ := ck["domain"].(string) if cookieDomainMatchesHost(dom, host) { filtered = append(filtered, ck) } } cookies = filtered } } // Capturar localStorage y sessionStorage del origen actualmente cargado. state := CdpStorageState{ Cookies: cookies, LocalStorage: readWebStorage(c, "localStorage"), SessionStorage: readWebStorage(c, "sessionStorage"), } data, err := json.MarshalIndent(state, "", " ") if err != nil { return fmt.Errorf("cdp save storage state: marshal: %w", err) } if err := os.WriteFile(outPath, data, 0644); err != nil { return fmt.Errorf("cdp save storage state: escribir archivo: %w", err) } return nil }