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"` } // isStorageAccessDenied reconoce el error de CdpEvaluate cuando el origen no // permite acceder a window.localStorage/sessionStorage (about:blank, chrome://, // data:, sandbox sin allow-same-origin): el navegador lanza SecurityError. Es // puro: decide a partir del texto del error. Distingue ese caso legítimo (no hay // storage que guardar -> {}) de un error real (conexión caída, JS roto) que SÍ // debe propagarse para no escribir una sesión incompleta en silencio. func isStorageAccessDenied(err error) bool { if err == nil { return false } s := err.Error() return strings.Contains(s, "SecurityError") || strings.Contains(s, "Access is denied") || strings.Contains(s, "operation is insecure") || strings.Contains(s, "denied for this document") } // readWebStorage lee window. (localStorage|sessionStorage) como mapa. // Distingue tres casos: // - storage accesible (con o sin datos) -> (mapa, nil) // - origen sin storage accesible (about:blank, chrome://) -> ({}, nil) // - error REAL de evaluación (conexión caída, JS roto, JSON inválido) -> (nil, error) func readWebStorage(c *CDPConn, store string) (map[string]string, error) { raw, err := CdpEvaluate(c, "JSON.stringify(Object.assign({}, window."+store+"))") if err != nil { if isStorageAccessDenied(err) { // Origen sin storage accesible: vacío legítimo, no error. return map[string]string{}, nil } return nil, fmt.Errorf("leer %s: %w", store, err) } if raw == "" || raw == "undefined" || raw == "null" { return map[string]string{}, nil } var m map[string]string if err := json.Unmarshal([]byte(raw), &m); err != nil { return nil, fmt.Errorf("parsear %s: %w", store, err) } return m, nil } // 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. Un // error real (no un origen sin storage) aborta el guardado: mejor fallar que // escribir una sesión incompleta que el caller creería válida. localStorage, err := readWebStorage(c, "localStorage") if err != nil { return fmt.Errorf("cdp save storage state: %w", err) } sessionStorage, err := readWebStorage(c, "sessionStorage") if err != nil { return fmt.Errorf("cdp save storage state: %w", err) } state := CdpStorageState{ Cookies: cookies, LocalStorage: localStorage, SessionStorage: 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 }