Initial commit: navegator - Chrome CDP automation for LLMs
Add complete navegator system for stealthy browser automation: - CDP client with WebSocket communication - Browser API with navigation, storage, network, runtime - Stealth flags and anti-detection scripts - Persistent profile support - Examples and comprehensive documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user