-
Notifications
You must be signed in to change notification settings - Fork 17
/
actions.go
391 lines (325 loc) · 11.1 KB
/
actions.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package chromedpundetected
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"math/rand"
"os"
"time"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/input"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
"github.com/Davincible/chromedp-undetected/util/easyjson"
)
// Cookie is used to set browser cookies.
type Cookie struct {
Name string `json:"name" yaml:"name"`
Value string `json:"value" yaml:"value"`
Domain string `json:"domain" yaml:"domain"`
Path string `json:"path" yaml:"path"`
Expires float64 `json:"expires" yaml:"expires"`
HTTPOnly bool `json:"httpOnly" yaml:"httpOnly"`
Secure bool `json:"secure" yaml:"secure"`
}
// UserAgentOverride overwrites the Chrome user agent.
//
// It's better to use this method than emulation.UserAgentOverride.
func UserAgentOverride(userAgent string) chromedp.ActionFunc {
return func(ctx context.Context) error {
return cdp.Execute(ctx, "Network.setUserAgentOverride",
emulation.SetUserAgentOverride(userAgent), nil)
}
}
// LoadCookiesFromFile takes a file path to a json file containing cookies, and
// loads in the cookies into the browser.
func LoadCookiesFromFile(path string) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
f, err := os.Open(path) //nolint:gosec
if err != nil {
return fmt.Errorf("failed to open file '%s': %w", path, err)
}
data, err := io.ReadAll(f)
if err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
var cookies []Cookie
if err := json.Unmarshal(data, &cookies); err != nil {
return fmt.Errorf("unmarshal cookies from json: %w", err)
}
return LoadCookies(cookies)(ctx)
})
}
// LoadCookies will load a set of cookies into the browser.
func LoadCookies(cookies []Cookie) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
for _, cookie := range cookies {
expiry := cdp.TimeSinceEpoch(time.Unix(int64(cookie.Expires), 0))
if err := network.SetCookie(cookie.Name, cookie.Value).
WithHTTPOnly(cookie.HTTPOnly).
WithSecure(cookie.Secure).
WithDomain(cookie.Domain).
WithPath(cookie.Path).
WithExpires(&expiry).
Do(ctx); err != nil {
return err
}
}
return nil
})
}
// SaveCookies extracts the cookies from the current URL and appends them to
// provided array.
func SaveCookies(cookies *[]Cookie) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
c, err := network.GetCookies().Do(ctx)
if err != nil {
return err
}
for _, cookie := range c {
*cookies = append(*cookies, Cookie{
Name: cookie.Name,
Value: cookie.Value,
Domain: cookie.Domain,
Path: cookie.Path,
Expires: cookie.Expires,
HTTPOnly: cookie.HTTPOnly,
Secure: cookie.HTTPOnly,
})
}
return nil
})
}
// SaveCookiesTo extracts the cookies from the current page and saves them
// as JSON to the provided path.
func SaveCookiesTo(path string) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
var c []Cookie
if err := SaveCookies(&c).Do(ctx); err != nil {
return err
}
b, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(path, b, 0644); err != nil { //nolint:gosec
return err
}
return nil
})
}
// RunCommandWithRes runs any Chrome Dev Tools command, with any params and
// sets the result to the res parameter. Make sure it is a pointer.
//
// In contrast to the native method of chromedp, with this method you can directly
// pass in a map with the data passed to the command.
func RunCommandWithRes(method string, params, res any) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
i := easyjson.New(params)
o := easyjson.New(res)
return cdp.Execute(ctx, method, i, o)
})
}
// RunCommand runs any Chrome Dev Tools command, with any params.
//
// In contrast to the native method of chromedp, with this method you can directly
// pass in a map with the data passed to the command.
func RunCommand(method string, params any) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
i := easyjson.New(params)
return cdp.Execute(ctx, method, i, nil)
})
}
// BlockURLs blocks a set of URLs in Chrome.
func BlockURLs(url ...string) chromedp.ActionFunc {
return RunCommand("Network.setBlockedURLs", map[string][]string{"urls": url})
}
// SendKeys does the same as chromedp.SendKeys excepts it randomly waits 100-500ms
// between sending key presses.
func SendKeys(sel any, v string, opts ...chromedp.QueryOption) chromedp.ActionFunc {
return chromedp.ActionFunc(func(ctx context.Context) error {
for _, key := range v {
if err := chromedp.SendKeys(sel, string(key), opts...).Do(ctx); err != nil {
return err
}
s := rand.Int63n(100) + 100 //nolint:gosec
time.Sleep(time.Duration(s) * time.Millisecond)
}
return nil
})
}
var (
removeMouseVisualsJS = `window.removeEventListener('mousemove', drawDotAtCoords);`
// addMouseVisualsJS adds a mouse event listener to the page that draws a
// red dot at the current mouse position to visualize the mouse path.
addMouseVisualsJS = `
function drawDotAtCoords(event) {
var x = event.clientX;
var y = event.clientY;
// Create a dot
var dot = document.createElement("div");
var dotSize = 8; // Set to 2px to make a small dot
dot.style.width = dotSize + "px";
dot.style.height = dotSize + "px";
dot.style.backgroundColor = "red";
dot.style.position = "absolute";
dot.style.top = (y - dotSize/2) + "px"; // Adjusting by half the size to center it
dot.style.left = (x - dotSize/2) + "px"; // Adjusting by half the size to center it
dot.style.borderRadius = "50%";
dot.style.pointerEvents = "none"; // So it doesn't interfere with other mouse events
dot.style.padding = "0"; // Setting padding to zero
dot.style.margin = "0"; // Setting margin to zero
dot.style.transition = "opacity 1s"; // Setting transition for fading effect
document.body.appendChild(dot);
// Fade out the dot after a delay
setTimeout(function() {
dot.style.opacity = "0";
// Remove the dot from the DOM after it's fully faded
setTimeout(function() {
dot.remove();
}, 10000);
}, 3000);
}
window.addEventListener('mousemove', drawDotAtCoords);
`
// mouseTrackingJS adds a global state of the last mouse position so we can
// start moving from the current mouse position instead of 0,0.
mouseTrackingJS = `
// Global storage on window object for mouse position
window.globalMousePos = { x: 0, y: 0 };
window.addEventListener('mousemove', (event) => {
const x = event.x;
const y = event.y;
if (x > 0 || y > 0) {
window.globalMousePos = { x, y };
}
console.log(x, y, event);
});
// Function to get the current mouse position or default to zero
function getCurrentMousePosition() {
return window.globalMousePos || { x: 0, y: 0 };
}
`
)
// MouseMoveOptions contains options for mouse movement.
type MouseMoveOptions struct {
steps int
delayMin time.Duration
delayMax time.Duration
randomJitter float64
visualizeMouse bool
}
// Default values for mouse movement.
var defaultMouseMoveOptions = MouseMoveOptions{
steps: 20,
delayMin: 5 * time.Millisecond,
delayMax: 50 * time.Millisecond,
randomJitter: 3,
visualizeMouse: false,
}
// MoveOptionSetter defines a function type to set mouse move options.
type MoveOptionSetter func(*MouseMoveOptions)
// WithSteps returns a MoveOptionSetter that sets the number of steps for the mouse movement.
func WithSteps(s int) MoveOptionSetter {
return func(opt *MouseMoveOptions) {
opt.steps = s
}
}
// WithDelayRange returns a MoveOptionSetter that sets the delay range between steps.
func WithDelayRange(min, max time.Duration) MoveOptionSetter {
return func(opt *MouseMoveOptions) {
opt.delayMin = min
opt.delayMax = max
}
}
// WithRandomJitter returns a MoveOptionSetter that sets the random jitter to introduce in mouse movement.
func WithRandomJitter(jitter float64) MoveOptionSetter {
return func(opt *MouseMoveOptions) {
opt.randomJitter = jitter
}
}
// WithVisualizeMouse returns a MoveOptionSetter that enables mouse movement visualization.
func WithVisualizeMouse() MoveOptionSetter {
return func(opt *MouseMoveOptions) {
opt.visualizeMouse = true
}
}
// MoveMouseToPosition moves the mouse to the given position, mimic random human mouse movements.
//
// If desired you can tweak the mouse movement behavior, defaults are set to mimic human mouse movements.
func MoveMouseToPosition(x, y float64, setters ...MoveOptionSetter) chromedp.ActionFunc { //nolint:varnamelen
options := defaultMouseMoveOptions
for _, setter := range setters {
setter(&options)
}
return func(ctx context.Context) error {
var pos struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
if err := chromedp.Evaluate(`getCurrentMousePosition()`, &pos).Do(ctx); err != nil {
if err := chromedp.Evaluate(mouseTrackingJS, nil).Do(ctx); err != nil {
return fmt.Errorf("inject mouse position tracing js: %w", err)
}
}
// Add mouse visualization event listener if enabled.
if options.visualizeMouse {
if err := chromedp.Evaluate(addMouseVisualsJS, nil).Do(ctx); err != nil {
return fmt.Errorf("inject mouse visualization js: %w", err)
}
// Remove mouse visualization event listere after mouse movemvent is complete.
defer func() {
chromedp.Evaluate(removeMouseVisualsJS, nil).Do(ctx) //nolint:errcheck,gosec
}()
}
// Generate control points for Bezier curve.
control1 := point{
x: pos.X + rand.Float64()*math.Max(x-pos.X, (y-pos.Y)/2),
y: pos.Y + rand.Float64()*math.Max(y-pos.Y, (x-pos.X)/2),
}
control2 := point{
x: x - rand.Float64()*(x-pos.X),
y: y - rand.Float64()*(y-pos.Y),
}
start := point{x: pos.X, y: pos.Y}
end := point{x: x, y: y}
for i := 0; i <= options.steps; i++ {
t := float64(i) / float64(options.steps)
point := bezierCubic(start, control1, control2, end, t)
targetX := point.x + rand.Float64()*options.randomJitter - options.randomJitter
targetY := point.y + rand.Float64()*options.randomJitter - options.randomJitter
p := &input.DispatchMouseEventParams{
Type: input.MouseMoved,
X: targetX,
Y: targetY,
Button: input.None,
}
if err := p.Do(ctx); err != nil {
return err
}
sleepDuration := options.delayMin + time.Duration(rand.Int63n(int64(options.delayMax-options.delayMin)))
time.Sleep(sleepDuration)
}
return nil
}
}
type point struct {
x, y float64
}
// Returns a point along a cubic Bézier curve.
// t is the "progress" along the curve, should be between 0 and 1.
func bezierCubic(p0, p1, p2, p3 point, t float64) point {
mt := 1 - t
mt2 := mt * mt
t2 := t * t
return point{
x: mt2*mt*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t2*t*p3.x,
y: mt2*mt*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t2*t*p3.y,
}
}