mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-13 18:50:47 +02:00
patch movements and console.log/debugger
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
1 parent
1c30639a2b
commit
50a440c744
4 files changed
+139
-18
No files matched your search
@@ -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{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user