package browser import ( "encoding/json" "fmt" "net/http" "net/url" ) // CdpTab representa una pestaña/target devuelta por el endpoint /json de CDP. // Campos publicos para que apps consumidoras puedan filtrar/render. type CdpTab struct { ID string `json:"id"` Type string `json:"type"` // "page", "iframe", "service_worker", ... Title string `json:"title"` URL string `json:"url"` WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` DevtoolsFrontendURL string `json:"devtoolsFrontendUrl,omitempty"` } // CdpListTabs llama GET http://{host}:{port}/json y retorna la lista de // targets. Sin filtrar por tipo — el caller decide si se queda solo con // type=="page", incluye iframes, etc. // // host vacio = "localhost". No requiere websocket (CDP expone /json en HTTP). func CdpListTabs(host string, port int) ([]CdpTab, error) { if host == "" { host = "localhost" } resp, err := http.Get(fmt.Sprintf("http://%s:%d/json", host, port)) if err != nil { return nil, fmt.Errorf("cdp list tabs: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("cdp list tabs: status %d", resp.StatusCode) } var tabs []CdpTab if err := json.NewDecoder(resp.Body).Decode(&tabs); err != nil { return nil, fmt.Errorf("cdp list tabs: decode: %w", err) } return tabs, nil } // CdpNewTab abre una pestaña nueva via PUT /json/new?. Si startURL // es vacio Chrome abre about:blank. Retorna el CdpTab recien creado. // // Nota: desde Chrome 126 /json/new requiere PUT (no GET). Mantenemos el // fallback a GET por compatibilidad con builds antiguos. func CdpNewTab(host string, port int, startURL string) (CdpTab, error) { if host == "" { host = "localhost" } endpoint := fmt.Sprintf("http://%s:%d/json/new", host, port) if startURL != "" { endpoint += "?" + url.QueryEscape(startURL) } tryRequest := func(method string) (CdpTab, error) { var out CdpTab req, err := http.NewRequest(method, endpoint, nil) if err != nil { return out, err } resp, err := http.DefaultClient.Do(req) if err != nil { return out, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return out, fmt.Errorf("status %d", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return out, fmt.Errorf("decode: %w", err) } return out, nil } tab, err := tryRequest(http.MethodPut) if err == nil && tab.ID != "" { return tab, nil } // Fallback GET (Chrome < 126). tab, err2 := tryRequest(http.MethodGet) if err2 == nil && tab.ID != "" { return tab, nil } if err == nil { err = err2 } return CdpTab{}, fmt.Errorf("cdp new tab: %w", err) } // CdpCloseTab cierra una pestaña por su ID via GET /json/close/. // Util complemento — incluido aqui porque comparte estructura HTTP /json. func CdpCloseTab(host string, port int, tabID string) error { if host == "" { host = "localhost" } if tabID == "" { return fmt.Errorf("cdp close tab: tabID vacio") } resp, err := http.Get(fmt.Sprintf("http://%s:%d/json/close/%s", host, port, url.PathEscape(tabID))) if err != nil { return fmt.Errorf("cdp close tab: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("cdp close tab: status %d", resp.StatusCode) } return nil } // CdpActivateTab pone la pestaña en foreground (focus) via /json/activate/. func CdpActivateTab(host string, port int, tabID string) error { if host == "" { host = "localhost" } if tabID == "" { return fmt.Errorf("cdp activate tab: tabID vacio") } resp, err := http.Get(fmt.Sprintf("http://%s:%d/json/activate/%s", host, port, url.PathEscape(tabID))) if err != nil { return fmt.Errorf("cdp activate tab: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("cdp activate tab: status %d", resp.StatusCode) } return nil }