feat(browser): funciones anti-deteccion + perfiles para web_scraping
Funciones nuevas del dominio browser (grupo navegator): - cdp_move_mouse_human / cdp_click_human: movimiento de raton con curva de Bezier cubica, easing y micro-jitter para imitar comportamiento humano y reducir deteccion de automatizacion. - cdp_wait_idle: espera network-idle contando requests en vuelo via eventos CDP Network.*; inmune a extensiones que mutan el DOM (Dark Reader, uBlock) y a animaciones JS. - list_chrome_profiles: lista perfiles de un user-data-dir (extensiones, nombre legible, preferencias). - prepare_chrome_profile (bash): clona un user-data-dir conservando solo una whitelist de extensiones (default uBlock Origin Lite). Modificadas: - chrome_launch: Linux-first (chromium/google-chrome/brave antes que chrome.exe), KeepExtensions y Setpgid para matar el arbol con cdp_close. - cdp_close: kill por grupo de proceso. Todas con tests verdes (go test ./functions/browser ok).
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CdpWaitIdleOpts configura el comportamiento de CdpWaitIdle.
|
||||
type CdpWaitIdleOpts struct {
|
||||
QuietMs int // ms que inflight debe permanecer <= MaxInflight (default 500)
|
||||
Timeout time.Duration // maximo total a esperar (default 8s)
|
||||
MaxInflight int // requests en vuelo tolerados para considerar idle (default 0)
|
||||
PollMs int // intervalo de chequeo en ms (default 100)
|
||||
}
|
||||
|
||||
// CdpWaitIdle espera a que la actividad de red de la pagina llegue a idle.
|
||||
// Suscribe eventos Network.requestWillBeSent / Network.loadingFinished /
|
||||
// Network.loadingFailed via el mecanismo OnEvent del CDPConn para mantener
|
||||
// un contador de requests en vuelo (inflight). Cuando inflight <= MaxInflight
|
||||
// de forma continuada durante QuietMs milisegundos, la funcion retorna nil.
|
||||
// Si se alcanza Timeout sin lograr esa ventana quieta, retorna error con el
|
||||
// inflight actual en el mensaje.
|
||||
//
|
||||
// Inmune a extensiones que mutan el DOM (Dark Reader, uBlock) y a animaciones
|
||||
// JS, ya que la señal es red, no DOM.
|
||||
func CdpWaitIdle(c *CDPConn, opts CdpWaitIdleOpts) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp wait idle: conexion nula")
|
||||
}
|
||||
|
||||
// Aplicar defaults.
|
||||
if opts.QuietMs <= 0 {
|
||||
opts.QuietMs = 500
|
||||
}
|
||||
if opts.Timeout <= 0 {
|
||||
opts.Timeout = 8 * time.Second
|
||||
}
|
||||
// MaxInflight 0 es el default semantico: queremos red completamente idle.
|
||||
if opts.PollMs <= 0 {
|
||||
opts.PollMs = 100
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
inflight int
|
||||
)
|
||||
|
||||
// Suscribir eventos Network usando el mismo mecanismo que cdp_har_record:
|
||||
// c.OnEvent retorna una funcion cancel que des-registra el handler.
|
||||
// Multiples consumidores del mismo metodo son soportados (slice de handlers).
|
||||
cancel1 := c.OnEvent("Network.requestWillBeSent", func(_ string, p map[string]any) {
|
||||
mu.Lock()
|
||||
inflight++
|
||||
mu.Unlock()
|
||||
})
|
||||
defer cancel1()
|
||||
|
||||
cancel2 := c.OnEvent("Network.loadingFinished", func(_ string, p map[string]any) {
|
||||
mu.Lock()
|
||||
if inflight > 0 {
|
||||
inflight--
|
||||
}
|
||||
mu.Unlock()
|
||||
})
|
||||
defer cancel2()
|
||||
|
||||
cancel3 := c.OnEvent("Network.loadingFailed", func(_ string, p map[string]any) {
|
||||
mu.Lock()
|
||||
if inflight > 0 {
|
||||
inflight--
|
||||
}
|
||||
mu.Unlock()
|
||||
})
|
||||
defer cancel3()
|
||||
|
||||
// Habilitar dominio Network (igual que cdp_har_record).
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return fmt.Errorf("cdp wait idle: Network.enable: %w", err)
|
||||
}
|
||||
defer c.sendCDP("Network.disable", nil) //nolint:errcheck
|
||||
|
||||
deadline := time.Now().Add(opts.Timeout)
|
||||
pollInterval := time.Duration(opts.PollMs) * time.Millisecond
|
||||
quietThreshold := time.Duration(opts.QuietMs) * time.Millisecond
|
||||
|
||||
var quietSince time.Time
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
time.Sleep(pollInterval)
|
||||
|
||||
mu.Lock()
|
||||
current := inflight
|
||||
mu.Unlock()
|
||||
|
||||
if current <= opts.MaxInflight {
|
||||
// Red idle: iniciar o mantener la ventana de quietud.
|
||||
if quietSince.IsZero() {
|
||||
quietSince = time.Now()
|
||||
}
|
||||
if time.Since(quietSince) >= quietThreshold {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Actividad detectada: reiniciar ventana.
|
||||
quietSince = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
current := inflight
|
||||
mu.Unlock()
|
||||
return fmt.Errorf("cdp wait idle: red no alcanzo idle despues de %s (inflight=%d)", opts.Timeout, current)
|
||||
}
|
||||
Reference in New Issue
Block a user