package browser import ( "context" "encoding/json" "fmt" "sync" ) // RequestInterceptor intercepta y puede modificar requests. type RequestInterceptor struct { URLPattern string Handler RequestHandler } // RequestHandler maneja un request interceptado. type RequestHandler func(req *InterceptedRequest) *RequestAction // InterceptedRequest representa un request interceptado. type InterceptedRequest struct { InterceptionID string RequestID string URL string Method string Headers map[string]interface{} PostData string ResourceType string } // RequestAction es la acción a tomar sobre un request. type RequestAction struct { // Continue continúa el request sin modificar Continue bool // Abort aborta el request Abort bool // Mock responde con datos mockeados Mock *MockResponse // ModifiedHeaders headers modificados ModifiedHeaders map[string]string // ModifiedURL URL modificada ModifiedURL string } // MockResponse es una respuesta mockeada. type MockResponse struct { StatusCode int Headers map[string]string Body string } // NetworkInterceptor gestiona interceptación de red. type NetworkInterceptor struct { browser *Browser interceptors []*RequestInterceptor mu sync.RWMutex enabled bool } // EnableNetworkInterception habilita la interceptación de red. func (b *Browser) EnableNetworkInterception(ctx context.Context) (*NetworkInterceptor, error) { ni := &NetworkInterceptor{ browser: b, interceptors: make([]*RequestInterceptor, 0), enabled: false, } // Habilitar Fetch domain if err := b.cdpClient.Execute(ctx, "Fetch.enable", map[string]interface{}{ "patterns": []map[string]interface{}{ { "urlPattern": "*", "resourceType": "*", }, }, }, nil); err != nil { return nil, fmt.Errorf("failed to enable Fetch domain: %w", err) } // Registrar handler de eventos b.cdpClient.On("Fetch.requestPaused", func(params json.RawMessage) { ni.handleRequestPaused(params) }) ni.enabled = true return ni, nil } // AddInterceptor agrega un interceptor. func (ni *NetworkInterceptor) AddInterceptor(urlPattern string, handler RequestHandler) { ni.mu.Lock() defer ni.mu.Unlock() ni.interceptors = append(ni.interceptors, &RequestInterceptor{ URLPattern: urlPattern, Handler: handler, }) } // handleRequestPaused maneja eventos de request pausado. func (ni *NetworkInterceptor) handleRequestPaused(params json.RawMessage) { var event struct { RequestID string `json:"requestId"` Request struct { URL string `json:"url"` Method string `json:"method"` Headers map[string]interface{} `json:"headers"` PostData string `json:"postData,omitempty"` } `json:"request"` ResourceType string `json:"resourceType"` FrameID string `json:"frameId"` InterceptionID string `json:"interceptionId"` } if err := json.Unmarshal(params, &event); err != nil { ni.continueRequest(event.InterceptionID) return } req := &InterceptedRequest{ InterceptionID: event.InterceptionID, RequestID: event.RequestID, URL: event.Request.URL, Method: event.Request.Method, Headers: event.Request.Headers, PostData: event.Request.PostData, ResourceType: event.ResourceType, } // Buscar interceptor que coincida ni.mu.RLock() var matchedHandler RequestHandler for _, interceptor := range ni.interceptors { if matchesPattern(req.URL, interceptor.URLPattern) { matchedHandler = interceptor.Handler break } } ni.mu.RUnlock() if matchedHandler == nil { // No hay interceptor, continuar normalmente ni.continueRequest(event.InterceptionID) return } // Ejecutar handler action := matchedHandler(req) ni.handleAction(req, action) } // handleAction ejecuta la acción sobre el request. func (ni *NetworkInterceptor) handleAction(req *InterceptedRequest, action *RequestAction) { ctx := context.Background() if action.Abort { // Abortar request params := map[string]interface{}{ "requestId": req.InterceptionID, "errorReason": "Aborted", } ni.browser.cdpClient.Execute(ctx, "Fetch.failRequest", params, nil) return } if action.Mock != nil { // Responder con mock params := map[string]interface{}{ "requestId": req.InterceptionID, "responseCode": action.Mock.StatusCode, "responseHeaders": convertHeaders(action.Mock.Headers), "body": base64Encode(action.Mock.Body), } ni.browser.cdpClient.Execute(ctx, "Fetch.fulfillRequest", params, nil) return } // Continuar (posiblemente modificado) params := map[string]interface{}{ "requestId": req.InterceptionID, } if action.ModifiedURL != "" { params["url"] = action.ModifiedURL } if len(action.ModifiedHeaders) > 0 { params["headers"] = convertHeaders(action.ModifiedHeaders) } ni.browser.cdpClient.Execute(ctx, "Fetch.continueRequest", params, nil) } // continueRequest continúa un request sin modificaciones. func (ni *NetworkInterceptor) continueRequest(interceptionID string) { ctx := context.Background() params := map[string]interface{}{ "requestId": interceptionID, } ni.browser.cdpClient.Execute(ctx, "Fetch.continueRequest", params, nil) } // Disable deshabilita la interceptación de red. func (ni *NetworkInterceptor) Disable(ctx context.Context) error { if !ni.enabled { return nil } if err := ni.browser.cdpClient.Execute(ctx, "Fetch.disable", nil, nil); err != nil { return fmt.Errorf("failed to disable Fetch domain: %w", err) } ni.enabled = false return nil } // BlockURLs bloquea requests a URLs que coincidan con los patrones. func (b *Browser) BlockURLs(ctx context.Context, patterns ...string) (*NetworkInterceptor, error) { ni, err := b.EnableNetworkInterception(ctx) if err != nil { return nil, err } for _, pattern := range patterns { ni.AddInterceptor(pattern, func(req *InterceptedRequest) *RequestAction { return &RequestAction{Abort: true} }) } return ni, nil } // BlockResourceTypes bloquea tipos de recursos específicos. func (b *Browser) BlockResourceTypes(ctx context.Context, resourceTypes ...string) (*NetworkInterceptor, error) { ni, err := b.EnableNetworkInterception(ctx) if err != nil { return nil, err } typeMap := make(map[string]bool) for _, rt := range resourceTypes { typeMap[rt] = true } ni.AddInterceptor("*", func(req *InterceptedRequest) *RequestAction { if typeMap[req.ResourceType] { return &RequestAction{Abort: true} } return &RequestAction{Continue: true} }) return ni, nil } // ModifyHeaders crea un interceptor que modifica headers. func (b *Browser) ModifyHeaders(ctx context.Context, headers map[string]string) (*NetworkInterceptor, error) { ni, err := b.EnableNetworkInterception(ctx) if err != nil { return nil, err } ni.AddInterceptor("*", func(req *InterceptedRequest) *RequestAction { return &RequestAction{ Continue: true, ModifiedHeaders: headers, } }) return ni, nil } // SetExtraHTTPHeaders establece headers HTTP extra para todos los requests. // Más eficiente que usar interceptación. func (b *Browser) SetExtraHTTPHeaders(ctx context.Context, headers map[string]string) error { params := map[string]interface{}{ "headers": headers, } return b.cdpClient.Execute(ctx, "Network.setExtraHTTPHeaders", params, nil) } // SetUserAgent establece el user agent. func (b *Browser) SetUserAgent(ctx context.Context, userAgent string) error { params := map[string]interface{}{ "userAgent": userAgent, } return b.cdpClient.Execute(ctx, "Network.setUserAgentOverride", params, nil) } // EmulateNetworkConditions emula condiciones de red (throttling). func (b *Browser) EmulateNetworkConditions(ctx context.Context, offline bool, latency, downloadThroughput, uploadThroughput float64) error { params := map[string]interface{}{ "offline": offline, "latency": latency, "downloadThroughput": downloadThroughput, "uploadThroughput": uploadThroughput, } return b.cdpClient.Execute(ctx, "Network.emulateNetworkConditions", params, nil) } // DisableCache deshabilita el caché HTTP. func (b *Browser) DisableCache(ctx context.Context) error { params := map[string]interface{}{ "cacheDisabled": true, } return b.cdpClient.Execute(ctx, "Network.setCacheDisabled", params, nil) } // EnableCache habilita el caché HTTP. func (b *Browser) EnableCache(ctx context.Context) error { params := map[string]interface{}{ "cacheDisabled": false, } return b.cdpClient.Execute(ctx, "Network.setCacheDisabled", params, nil) } // Helpers func matchesPattern(url, pattern string) bool { // Implementación simple de pattern matching // * = wildcard if pattern == "*" { return true } // TODO: Implementar matching más sofisticado con wildcards // Por ahora, solo match exacto o wildcard completo return url == pattern } func convertHeaders(headers map[string]string) []map[string]string { result := make([]map[string]string, 0, len(headers)) for name, value := range headers { result = append(result, map[string]string{ "name": name, "value": value, }) } return result } func base64Encode(s string) string { // Simple base64 encode usando tabla estándar const base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" data := []byte(s) result := make([]byte, ((len(data)+2)/3)*4) j := 0 for i := 0; i < len(data); i += 3 { b := (uint(data[i]) << 16) if i+1 < len(data) { b |= (uint(data[i+1]) << 8) } if i+2 < len(data) { b |= uint(data[i+2]) } result[j] = base64Table[(b>>18)&0x3F] result[j+1] = base64Table[(b>>12)&0x3F] if i+1 < len(data) { result[j+2] = base64Table[(b>>6)&0x3F] } else { result[j+2] = '=' } if i+2 < len(data) { result[j+3] = base64Table[b&0x3F] } else { result[j+3] = '=' } j += 4 } return string(result) }