patch movements and console.log/debugger

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing committed 2026-05-25 10:05:58 +02:00
1 parent 1c30639a2b
commit 50a440c744
4 files changed
+139 -18

No files matched your search

+21 -1
View File
@@ -9,6 +9,7 @@ import (
"image"
"image/draw"
"image/jpeg"
"math/rand"
"net/http"
"net/url"
"sync"
@@ -574,6 +575,11 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
// Read loop: forward victim events into the runner; route stream_input with coord offset.
go func() {
// Per-stream last mousemove dispatch time used to cap stream_input mousemove
// events at 60 Hz. Clicks and other actions are always forwarded immediately.
streamMouseMoveLast := map[string]time.Time{}
const streamMouseMoveMinInterval = 16 * time.Millisecond
for {
_, msg, err := conn.ReadMessage()
if err != nil {
@@ -614,6 +620,16 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
continue
}
if cmd.Type == "stream_input" && cmd.Name != "" && cmd.Action != "" {
// Rate-limit mousemove to 60 Hz - the captcha screencast frame rate
// is typically well below this, so extra events never make it into
// a frame and only add unnecessary CDP round-trips.
if cmd.Action == "mousemove" {
now := time.Now()
if now.Sub(streamMouseMoveLast[cmd.Name]) < streamMouseMoveMinInterval {
continue
}
streamMouseMoveLast[cmd.Name] = now
}
if val, exists := activeNamedStreams.Load(cmd.Name); exists {
si := val.(*streamInfo)
// cmd.X/Y are in cropped-canvas JPEG pixels; map back to CDP CSS coords.
@@ -1278,9 +1294,13 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) {
mods := int(cmd.Modifiers)
switch cmd.Type {
case "mousemove":
// Add ±0.5 px uniform noise so canvas-quantized coordinates have
// subpixel variation, matching natural pointer imprecision.
jx := cmd.X + (rand.Float64()*2-1)*0.5
jy := cmd.Y + (rand.Float64()*2-1)*0.5
proto.InputDispatchMouseEvent{
Type: proto.InputDispatchMouseEventTypeMouseMoved,
X: cmd.X, Y: cmd.Y, Modifiers: mods,
X: jx, Y: jy, Modifiers: mods,
}.Call(page) //nolint:errcheck
case "mousedown":
proto.InputDispatchMouseEvent{
+92 -1
View File
@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"regexp"
"strings"
"sync"
@@ -658,11 +660,100 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page,
return goja.Undefined()
})
// mouseX/Y tracks the last position dispatched by humanMoveTo so that
// consecutive moveMouse and clickXY calls produce a continuous path.
var mouseX, mouseY float64
// humanMoveTo moves the CDP cursor from the last tracked position to
// (targetX, targetY) along a cubic Bezier curve with ease-in-out timing
// and optional micro-jitter, mimicking natural hand movement.
// durationMs <= 0 picks a random value in [200, 400].
// jitterPx < 0 uses the default amplitude of 1.5 px.
humanMoveTo := func(targetX, targetY, durationMs, jitterPx float64) {
if durationMs <= 0 {
durationMs = 200 + rand.Float64()*200
}
if jitterPx < 0 {
jitterPx = 1.5
}
startX, startY := mouseX, mouseY
dx, dy := targetX-startX, targetY-startY
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 2 {
page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck
mouseX, mouseY = targetX, targetY
return
}
// Perpendicular unit vector for Bezier deviation.
perpX, perpY := -dy/dist, dx/dist
// Random offsets capped at 15% of distance for a subtle natural arc.
maxDev := dist * 0.15
c1x := startX + dx*0.33 + perpX*(rand.Float64()*2-1)*maxDev
c1y := startY + dy*0.33 + perpY*(rand.Float64()*2-1)*maxDev
c2x := startX + dx*0.67 + perpX*(rand.Float64()*2-1)*maxDev
c2y := startY + dy*0.67 + perpY*(rand.Float64()*2-1)*maxDev
steps := int(durationMs / 10)
if steps < 5 {
steps = 5
}
if steps > 60 {
steps = 60
}
stepDur := time.Duration(float64(time.Millisecond) * durationMs / float64(steps))
for i := 1; i <= steps; i++ {
select {
case <-page.GetContext().Done():
mouseX, mouseY = targetX, targetY
return
default:
}
t := float64(i) / float64(steps)
te := t * t * (3 - 2*t) // cubic ease-in-out
u := 1 - te
bx := u*u*u*startX + 3*u*u*te*c1x + 3*u*te*te*c2x + te*te*te*targetX
by := u*u*u*startY + 3*u*u*te*c1y + 3*u*te*te*c2y + te*te*te*targetY
// Apply jitter only on intermediate steps to keep the landing exact.
if jitterPx > 0 && i < steps {
bx += (rand.Float64()*2 - 1) * jitterPx
by += (rand.Float64()*2 - 1) * jitterPx
}
page.Mouse.MoveTo(proto.Point{X: bx, Y: by}) //nolint:errcheck
if i < steps {
time.Sleep(stepDur)
}
}
page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck
mouseX, mouseY = targetX, targetY
}
pc.Set("moveMouse", func(call goja.FunctionCall) goja.Value {
targetX := call.Argument(0).ToFloat()
targetY := call.Argument(1).ToFloat()
durationMs := -1.0
jitterPx := -1.0
if len(call.Arguments) > 2 {
if obj := call.Argument(2).ToObject(vm); obj != nil {
if v := obj.Get("duration"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
durationMs = v.ToFloat()
}
if v := obj.Get("jitter"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
jitterPx = v.ToFloat()
}
}
}
dbg(fmt.Sprintf("→ moveMouse %.0f,%.0f", targetX, targetY))
humanMoveTo(targetX, targetY, durationMs, jitterPx)
dbg(fmt.Sprintf("✓ moveMouse %.0f,%.0f", targetX, targetY))
return goja.Undefined()
})
pc.Set("clickXY", func(call goja.FunctionCall) goja.Value {
x := call.Argument(0).ToFloat()
y := call.Argument(1).ToFloat()
dbg(fmt.Sprintf("→ clickXY %.0f,%.0f", x, y))
must(page.Mouse.MoveTo(proto.Point{X: x, Y: y}))
humanMoveTo(x, y, -1, -1)
must(page.Mouse.Click(proto.InputMouseButtonLeft, 1))
dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y))
return goja.Undefined()
+14 -16
View File
@@ -659,16 +659,15 @@ func (r *Runner) Run(ctx context.Context) error {
browser.Close() //nolint:errcheck
return goja.Undefined()
}
/*
// ignore "debugger;" code lines explicitly
err = proto.DebuggerSetSkipAllPauses{Skip: true}.Call(page)
if err != nil {
emitter.errorf(fmt.Sprintf("skip debug failed: %v", err))
browser.Close() //nolint:errcheck
return goja.Undefined()
}
// patch console Log
_, err = page.EvalOnNewDocument(`() => {
// ignore "debugger;" code lines explicitly
err = proto.DebuggerSetSkipAllPauses{Skip: true}.Call(page)
if err != nil {
emitter.errorf(fmt.Sprintf("skip debug failed: %v", err))
browser.Close() //nolint:errcheck
return goja.Undefined()
}
// patch console Log
_, err = page.EvalOnNewDocument(`() => {
const noop = () => {};
console.log = noop;
@@ -678,12 +677,11 @@ func (r *Runner) Run(ctx context.Context) error {
console.info = noop;
console.warn = noop;
}`)
if err != nil {
emitter.errorf(fmt.Sprintf("console patch failed: %v", err))
browser.Close() //nolint:errcheck
return goja.Undefined()
}
*/
if err != nil {
emitter.errorf(fmt.Sprintf("console patch failed: %v", err))
browser.Close() //nolint:errcheck
return goja.Undefined()
}
}
// Apply user-agent override. When headless and no explicit UA is set we use
@@ -381,7 +381,18 @@ interface Session {
// ── Mouse ─────────────────────────────────────────────────────────────────
click(selector: string): void;
doubleClick(selector: string): void;
/**
* Move to absolute coordinates along a curved Bezier path with micro-jitter
* before clicking. Prefer this over bare clickXY when bot detection is a concern.
*/
clickXY(x: number, y: number): void;
/**
* Smoothly move the mouse to absolute page coordinates without clicking.
* Uses a cubic Bezier curve with ease-in-out timing and micro-jitter.
* @param opts.duration Movement duration in ms. Default: random 200-400 ms.
* @param opts.jitter Jitter amplitude in px (0 = none). Default: 1.5 px.
*/
moveMouse(x: number, y: number, opts?: { duration?: number; jitter?: number }): void;
scrollIntoView(selector: string): void;
// ── Keyboard ──────────────────────────────────────────────────────────────
@@ -530,6 +541,7 @@ interface FrameSession {
click(selector: string): void;
doubleClick(selector: string): void;
clickXY(x: number, y: number): void;
moveMouse(x: number, y: number, opts?: { duration?: number; jitter?: number }): void;
scrollIntoView(selector: string): void;
// ── Keyboard ──────────────────────────────────────────────────────────────