From e1e1d28bba202f78df02ae3b0bbb1f5a7c0d55b3 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 25 May 2026 10:21:32 +0200 Subject: [PATCH] improve Signed-off-by: Ronni Skansing --- backend/controller/remoteBrowser.go | 58 +++++++++++++++++++++------ backend/remotebrowser/browser.go | 61 ++++++++++++++++++++++++++--- 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go index c30112b..8a82c8c 100644 --- a/backend/controller/remoteBrowser.go +++ b/backend/controller/remoteBrowser.go @@ -9,6 +9,7 @@ import ( "image" "image/draw" "image/jpeg" + "math" "math/rand" "net/http" "net/url" @@ -1292,30 +1293,63 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { btn = proto.InputMouseButtonRight } mods := int(cmd.Modifiers) + // Shared Buttons bitmask values for pointer events. + zeroButtons := 0 + oneButton := 1 + nowTs := func() proto.TimeSinceEpoch { + return proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9) + } 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 + // Add ±1 px integer noise then round: keeps movementX == clientX-prevClientX + // consistent (subpixel CDP coordinates create a float/int mismatch detectors + // check), while still adding the ±1 px variation that breaks exact-integer paths. + jx := math.Round(cmd.X + (rand.Float64()*2-1)*0.5) + jy := math.Round(cmd.Y + (rand.Float64()*2-1)*0.5) proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseMoved, - X: jx, Y: jy, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMouseMoved, + X: jx, + Y: jy, + Modifiers: mods, + Timestamp: nowTs(), + Button: proto.InputMouseButtonNone, + Buttons: &zeroButtons, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "mousedown": proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMousePressed, - X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: cmd.X, + Y: cmd.Y, + Modifiers: mods, + Timestamp: nowTs(), + Button: btn, + Buttons: &oneButton, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "mouseup": proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseReleased, - X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: cmd.X, + Y: cmd.Y, + Modifiers: mods, + Timestamp: nowTs(), + Button: btn, + Buttons: &zeroButtons, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "scroll": proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseWheel, - X: cmd.X, Y: cmd.Y, DeltaX: cmd.DeltaX, DeltaY: cmd.DeltaY, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMouseWheel, + X: cmd.X, + Y: cmd.Y, + DeltaX: cmd.DeltaX, + DeltaY: cmd.DeltaY, + Modifiers: mods, + Timestamp: nowTs(), + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "keydown": proto.InputDispatchKeyEvent{ diff --git a/backend/remotebrowser/browser.go b/backend/remotebrowser/browser.go index e64b4ad..4418327 100644 --- a/backend/remotebrowser/browser.go +++ b/backend/remotebrowser/browser.go @@ -664,6 +664,27 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, // consecutive moveMouse and clickXY calls produce a continuous path. var mouseX, mouseY float64 + // cdpMouseMove dispatches a MouseMoved event with all required fields set. + // Using proto.InputDispatchMouseEvent directly (instead of page.Mouse.MoveTo) + // lets us set Timestamp, PointerType, and Buttons - fields that rod omits + // and that detectors use to distinguish CDP-injected events from hardware input. + zeroButtons := 0 + cdpMouseMove := func(x, y float64) { + // Round to integers: Chrome stores cursor position as float internally and + // computes movementX/Y from the float delta, but event.clientX/Y are integers. + // Subpixel coordinates create a movementX != clientX-prevClientX mismatch + // that detectors use as a CDP fingerprint. + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseMoved, + X: math.Round(x), + Y: math.Round(y), + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonNone, + Buttons: &zeroButtons, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + } + // 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. @@ -680,7 +701,7 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, 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 + cdpMouseMove(targetX, targetY) mouseX, mouseY = targetX, targetY return } @@ -719,12 +740,12 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, bx += (rand.Float64()*2 - 1) * jitterPx by += (rand.Float64()*2 - 1) * jitterPx } - page.Mouse.MoveTo(proto.Point{X: bx, Y: by}) //nolint:errcheck + cdpMouseMove(bx, by) if i < steps { time.Sleep(stepDur) } } - page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck + cdpMouseMove(targetX, targetY) mouseX, mouseY = targetX, targetY } @@ -750,11 +771,39 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, }) pc.Set("clickXY", func(call goja.FunctionCall) goja.Value { - x := call.Argument(0).ToFloat() - y := call.Argument(1).ToFloat() + x := math.Round(call.Argument(0).ToFloat()) + y := math.Round(call.Argument(1).ToFloat()) dbg(fmt.Sprintf("→ clickXY %.0f,%.0f", x, y)) humanMoveTo(x, y, -1, -1) - must(page.Mouse.Click(proto.InputMouseButtonLeft, 1)) + // Dispatch mousedown and mouseup directly so we can set Timestamp, + // PointerType, and Buttons - and add a realistic hold duration. + // page.Mouse.Click omits these fields and has 0 ms hold time. + oneButton := 1 + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: x, + Y: y, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonLeft, + Buttons: &oneButton, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + // Human click hold: 80-150 ms between press and release. + time.Sleep(time.Duration(80+rand.Intn(70)) * time.Millisecond) + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: x, + Y: y, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonLeft, + Buttons: &zeroButtons, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + // Sync rod's internal position tracker so any subsequent rod operations + // (e.g. el.Click) see the correct cursor location. + page.Mouse.MoveTo(proto.Point{X: x, Y: y}) //nolint:errcheck dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y)) return goja.Undefined() })