Files
phishingclub/backend/vendor/github.com/go-rod/rod/browser.go
T
Ronni Skansing f5e6f53d75 update vendor
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2026-05-24 09:57:07 +02:00

544 lines
14 KiB
Go

//go:generate go run ./lib/utils/setup
//go:generate go run ./lib/proto/generate
//go:generate go run ./lib/js/generate
//go:generate go run ./lib/assets/generate
//go:generate go run ./lib/utils/lint
// Package rod is a high-level driver directly based on DevTools Protocol.
package rod
import (
"context"
"reflect"
"strings"
"sync"
"time"
"github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/defaults"
"github.com/go-rod/rod/lib/devices"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/goob"
)
// Browser implements these interfaces.
var (
_ proto.Client = &Browser{}
_ proto.Contextable = &Browser{}
)
// Browser represents the browser.
// It doesn't depends on file system, it should work with remote browser seamlessly.
// To check the env var you can use to quickly enable options from CLI, check here:
// https://pkg.go.dev/github.com/go-rod/rod/lib/defaults
type Browser struct {
// BrowserContextID is the id for incognito window
BrowserContextID proto.BrowserBrowserContextID
e eFunc
ctx context.Context
sleeper func() utils.Sleeper
logger utils.Logger
slowMotion time.Duration // see defaults.slow
trace bool // see defaults.Trace
monitor string
defaultDevice devices.Device
controlURL string
client CDPClient
event *goob.Observable // all the browser events from cdp client
targetsLock *sync.Mutex
// stores all the previous cdp call of same type. Browser doesn't have enough API
// for us to retrieve all its internal states. This is an workaround to map them to local.
// For example you can't use cdp API to get the current position of mouse.
states *sync.Map
}
// New creates a controller.
// DefaultDevice to emulate is set to [devices.LaptopWithMDPIScreen].Landscape(), it will change the default
// user-agent and can make the actual view area smaller than the browser window on headful mode,
// you can use [Browser.NoDefaultDevice] to disable it.
func New() *Browser {
return (&Browser{
ctx: context.Background(),
sleeper: DefaultSleeper,
controlURL: defaults.URL,
slowMotion: defaults.Slow,
trace: defaults.Trace,
monitor: defaults.Monitor,
logger: DefaultLogger,
defaultDevice: devices.LaptopWithMDPIScreen.Landscape(),
targetsLock: &sync.Mutex{},
states: &sync.Map{},
}).WithPanic(utils.Panic)
}
// Incognito creates a new incognito browser.
func (b *Browser) Incognito() (*Browser, error) {
res, err := proto.TargetCreateBrowserContext{}.Call(b)
if err != nil {
return nil, err
}
incognito := *b
incognito.BrowserContextID = res.BrowserContextID
return &incognito, nil
}
// ControlURL set the url to remote control browser.
func (b *Browser) ControlURL(url string) *Browser {
b.controlURL = url
return b
}
// SlowMotion set the delay for each control action, such as the simulation of the human inputs.
func (b *Browser) SlowMotion(delay time.Duration) *Browser {
b.slowMotion = delay
return b
}
// Trace enables/disables the visual tracing of the input actions on the page.
func (b *Browser) Trace(enable bool) *Browser {
b.trace = enable
return b
}
// Monitor address to listen if not empty. Shortcut for [Browser.ServeMonitor].
func (b *Browser) Monitor(url string) *Browser {
b.monitor = url
return b
}
// Logger overrides the default log functions for tracing.
func (b *Browser) Logger(l utils.Logger) *Browser {
b.logger = l
return b
}
// Client set the cdp client.
func (b *Browser) Client(c CDPClient) *Browser {
b.client = c
return b
}
// DefaultDevice sets the default device for new page to emulate in the future.
// Default is [devices.LaptopWithMDPIScreen].
// Set it to [devices.Clear] to disable it.
func (b *Browser) DefaultDevice(d devices.Device) *Browser {
b.defaultDevice = d
return b
}
// NoDefaultDevice is the same as [Browser.DefaultDevice](devices.Clear).
func (b *Browser) NoDefaultDevice() *Browser {
return b.DefaultDevice(devices.Clear)
}
// Connect to the browser and start to control it.
// If fails to connect, try to launch a local browser, if local browser not found try to download one.
func (b *Browser) Connect() error {
if b.client == nil {
u := b.controlURL
if u == "" {
var err error
u, err = launcher.New().Context(b.ctx).Launch()
if err != nil {
return err
}
}
c, err := cdp.StartWithURL(b.ctx, u, nil)
if err != nil {
return err
}
b.client = c
} else if b.controlURL != "" {
panic("Browser.Client and Browser.ControlURL can't be set at the same time")
}
b.initEvents()
if b.monitor != "" {
launcher.Open(b.ServeMonitor(b.monitor))
}
return proto.TargetSetDiscoverTargets{Discover: true}.Call(b)
}
// Close the browser.
func (b *Browser) Close() error {
if b.BrowserContextID == "" {
return proto.BrowserClose{}.Call(b)
}
return proto.TargetDisposeBrowserContext{BrowserContextID: b.BrowserContextID}.Call(b)
}
// Page creates a new browser tab. If opts.URL is empty, the default target will be "about:blank".
func (b *Browser) Page(opts proto.TargetCreateTarget) (p *Page, err error) {
req := opts
req.BrowserContextID = b.BrowserContextID
req.URL = "about:blank"
target, err := req.Call(b)
if err != nil {
return nil, err
}
defer func() {
// If Navigate or PageFromTarget fails we should close the target to prevent leak
if err != nil {
_, _ = proto.TargetCloseTarget{TargetID: target.TargetID}.Call(b)
}
}()
p, err = b.PageFromTarget(target.TargetID)
if err != nil {
return
}
if opts.URL == "" {
return
}
err = p.Navigate(opts.URL)
return
}
// Pages retrieves all visible pages.
func (b *Browser) Pages() (Pages, error) {
list, err := proto.TargetGetTargets{}.Call(b)
if err != nil {
return nil, err
}
pageList := Pages{}
for _, target := range list.TargetInfos {
if target.Type != proto.TargetTargetInfoTypePage {
continue
}
page, err := b.PageFromTarget(target.TargetID)
if err != nil {
return nil, err
}
pageList = append(pageList, page)
}
return pageList, nil
}
// Call implements the [proto.Client] to call raw cdp interface directly.
func (b *Browser) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) {
res, err = b.client.Call(ctx, sessionID, methodName, params)
if err != nil {
return nil, err
}
b.set(proto.TargetSessionID(sessionID), methodName, params)
return
}
// PageFromSession is used for low-level debugging.
func (b *Browser) PageFromSession(sessionID proto.TargetSessionID) *Page {
sessionCtx, cancel := context.WithCancel(b.ctx)
return &Page{
e: b.e,
ctx: sessionCtx,
sessionCancel: cancel,
sleeper: b.sleeper,
browser: b,
SessionID: sessionID,
}
}
// PageFromTarget gets or creates a Page instance.
func (b *Browser) PageFromTarget(targetID proto.TargetTargetID) (*Page, error) {
b.targetsLock.Lock()
defer b.targetsLock.Unlock()
page := b.loadCachedPage(targetID)
if page != nil {
return page, nil
}
session, err := proto.TargetAttachToTarget{
TargetID: targetID,
Flatten: true, // if it's not set no response will return
}.Call(b)
if err != nil {
return nil, err
}
sessionCtx, cancel := context.WithCancel(b.ctx)
page = &Page{
e: b.e,
ctx: sessionCtx,
sessionCancel: cancel,
sleeper: b.sleeper,
browser: b,
TargetID: targetID,
SessionID: session.SessionID,
FrameID: proto.PageFrameID(targetID),
jsCtxLock: &sync.Mutex{},
jsCtxID: new(proto.RuntimeRemoteObjectID),
helpersLock: &sync.Mutex{},
}
page.root = page
page.newKeyboard().newMouse().newTouch()
if !b.defaultDevice.IsClear() {
err = page.Emulate(b.defaultDevice)
if err != nil {
return nil, err
}
}
b.cachePage(page)
page.initEvents()
// If we don't enable it, it will cause a lot of unexpected browser behavior.
// Such as proto.PageAddScriptToEvaluateOnNewDocument won't work.
page.EnableDomain(&proto.PageEnable{})
return page, nil
}
// EachEvent is similar to [Page.EachEvent], but catches events of the entire browser.
func (b *Browser) EachEvent(callbacks ...interface{}) (wait func()) {
return b.eachEvent("", callbacks...)
}
// WaitEvent waits for the next event for one time. It will also load the data into the event object.
func (b *Browser) WaitEvent(e proto.Event) (wait func()) {
return b.waitEvent("", e)
}
// waits for the next event for one time. It will also load the data into the event object.
func (b *Browser) waitEvent(sessionID proto.TargetSessionID, e proto.Event) (wait func()) {
valE := reflect.ValueOf(e)
valTrue := reflect.ValueOf(true)
if valE.Kind() != reflect.Ptr {
valE = reflect.New(valE.Type())
}
// dynamically creates a function on runtime:
//
// func(ee proto.Event) bool {
// *e = *ee
// return true
// }
fnType := reflect.FuncOf([]reflect.Type{valE.Type()}, []reflect.Type{valTrue.Type()}, false)
fnVal := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
valE.Elem().Set(args[0].Elem())
return []reflect.Value{valTrue}
})
return b.eachEvent(sessionID, fnVal.Interface())
}
// If the any callback returns true the event loop will stop.
// It will enable the related domains if not enabled, and restore them after wait ends.
func (b *Browser) eachEvent(sessionID proto.TargetSessionID, callbacks ...interface{}) (wait func()) {
cbMap := map[string]reflect.Value{}
restores := []func(){}
for _, cb := range callbacks {
cbVal := reflect.ValueOf(cb)
eType := cbVal.Type().In(0)
name := reflect.New(eType.Elem()).Interface().(proto.Event).ProtoEvent() //nolint: forcetypeassert
cbMap[name] = cbVal
// Only enabled domains will emit events to cdp client.
// We enable the domains for the event types if it's not enabled.
// We restore the domains to their previous states after the wait ends.
domain, _ := proto.ParseMethodName(name)
if req := proto.GetType(domain + ".enable"); req != nil {
enable := reflect.New(req).Interface().(proto.Request) //nolint: forcetypeassert
restores = append(restores, b.EnableDomain(sessionID, enable))
}
}
b, cancel := b.WithCancel()
messages := b.Event()
return func() {
if messages == nil {
panic("can't use wait function twice")
}
defer func() {
cancel()
messages = nil
for _, restore := range restores {
restore()
}
}()
for msg := range messages {
if !(sessionID == "" || msg.SessionID == sessionID) {
continue
}
if cbVal, has := cbMap[msg.Method]; has {
e := reflect.New(proto.GetType(msg.Method))
msg.Load(e.Interface().(proto.Event)) //nolint: forcetypeassert
args := []reflect.Value{e}
if cbVal.Type().NumIn() == 2 {
args = append(args, reflect.ValueOf(msg.SessionID))
}
res := cbVal.Call(args)
if len(res) > 0 {
if res[0].Bool() {
return
}
}
}
}
}
}
// Event of the browser.
func (b *Browser) Event() <-chan *Message {
src := b.event.Subscribe(b.ctx)
dst := make(chan *Message)
go func() {
defer close(dst)
for {
select {
case <-b.ctx.Done():
return
case e, ok := <-src:
if !ok {
return
}
select {
case <-b.ctx.Done():
return
case dst <- e.(*Message): //nolint: forcetypeassert
}
}
}
}()
return dst
}
func (b *Browser) initEvents() {
ctx, cancel := context.WithCancel(b.ctx)
b.event = goob.New(ctx)
event := b.client.Event()
go func() {
defer cancel()
for e := range event {
b.event.Publish(&Message{
SessionID: proto.TargetSessionID(e.SessionID),
Method: e.Method,
lock: &sync.Mutex{},
data: e.Params,
})
}
}()
}
func (b *Browser) pageInfo(id proto.TargetTargetID) (*proto.TargetTargetInfo, error) {
res, err := proto.TargetGetTargetInfo{TargetID: id}.Call(b)
if err != nil {
return nil, err
}
return res.TargetInfo, nil
}
func (b *Browser) isHeadless() (enabled bool) {
res, _ := proto.BrowserGetBrowserCommandLine{}.Call(b)
for _, v := range res.Arguments {
if strings.Contains(v, "headless") {
return true
}
}
return false
}
// IgnoreCertErrors switch. If enabled, all certificate errors will be ignored.
func (b *Browser) IgnoreCertErrors(enable bool) error {
return proto.SecuritySetIgnoreCertificateErrors{Ignore: enable}.Call(b)
}
// GetCookies from the browser.
func (b *Browser) GetCookies() ([]*proto.NetworkCookie, error) {
res, err := proto.StorageGetCookies{BrowserContextID: b.BrowserContextID}.Call(b)
if err != nil {
return nil, err
}
return res.Cookies, nil
}
// SetCookies to the browser. If the cookies is nil it will clear all the cookies.
func (b *Browser) SetCookies(cookies []*proto.NetworkCookieParam) error {
if cookies == nil {
return proto.StorageClearCookies{BrowserContextID: b.BrowserContextID}.Call(b)
}
return proto.StorageSetCookies{
Cookies: cookies,
BrowserContextID: b.BrowserContextID,
}.Call(b)
}
// WaitDownload returns a helper to get the next download file.
// The file path will be:
//
// filepath.Join(dir, info.GUID)
func (b *Browser) WaitDownload(dir string) func() (info *proto.PageDownloadWillBegin) {
var oldDownloadBehavior proto.BrowserSetDownloadBehavior
has := b.LoadState("", &oldDownloadBehavior)
_ = proto.BrowserSetDownloadBehavior{
Behavior: proto.BrowserSetDownloadBehaviorBehaviorAllowAndName,
BrowserContextID: b.BrowserContextID,
DownloadPath: dir,
}.Call(b)
var start *proto.PageDownloadWillBegin
waitProgress := b.EachEvent(func(e *proto.PageDownloadWillBegin) {
start = e
}, func(e *proto.PageDownloadProgress) bool {
return start != nil && start.GUID == e.GUID && e.State == proto.PageDownloadProgressStateCompleted
})
return func() *proto.PageDownloadWillBegin {
defer func() {
if has {
_ = oldDownloadBehavior.Call(b)
} else {
_ = proto.BrowserSetDownloadBehavior{
Behavior: proto.BrowserSetDownloadBehaviorBehaviorDefault,
BrowserContextID: b.BrowserContextID,
}.Call(b)
}
}()
waitProgress()
return start
}
}
// Version info of the browser.
func (b *Browser) Version() (*proto.BrowserGetVersionResult, error) {
return proto.BrowserGetVersion{}.Call(b)
}