mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-05-15 21:28:17 +02:00
36ee621f1a
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
686 lines
21 KiB
Go
686 lines
21 KiB
Go
package remotebrowser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/go-rod/rod"
|
|
"github.com/go-rod/rod/lib/input"
|
|
"github.com/go-rod/rod/lib/proto"
|
|
)
|
|
|
|
// RegisterBrowserBindings wires up all rod actions as callable JS functions
|
|
// on the provided target object. All actions block until completion.
|
|
// emitter may be nil (e.g. in withTimeout sub-sessions); screenshots and debug logs
|
|
// are silently dropped when nil. debug adds before/after log lines for every action.
|
|
//
|
|
// Debug symbol legend:
|
|
//
|
|
// → action starting ✓ action done … waiting ? reading = result value
|
|
func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, emitter *channelEmitter, debug bool, queryTimeout int) {
|
|
must := func(err error) {
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
}
|
|
|
|
// argStr returns the string value of a goja argument, or "" for undefined/null.
|
|
argStr := func(v goja.Value) string {
|
|
if goja.IsUndefined(v) || goja.IsNull(v) {
|
|
return ""
|
|
}
|
|
return v.String()
|
|
}
|
|
|
|
dbg := func(msg string) {
|
|
if debug && emitter != nil {
|
|
emitter.log("[dbg] " + msg)
|
|
}
|
|
}
|
|
|
|
// readPage returns a page for read-only CDP queries (getNodeCount, getText,
|
|
// evaluate, screenshot, …). When queryTimeout > 0 it is bounded so a stalled
|
|
// browser can't freeze the goja listen loop. 0 means no extra timeout.
|
|
readPage := func() (*rod.Page, func()) {
|
|
if queryTimeout > 0 {
|
|
tCtx, cancel := context.WithTimeout(page.GetContext(), time.Duration(queryTimeout)*time.Millisecond)
|
|
return page.Context(tCtx), cancel
|
|
}
|
|
return page, func() {}
|
|
}
|
|
|
|
// pollUntil polls a JS condition (function expression) until it returns true,
|
|
// the page context is done, or the condition errors.
|
|
pollUntil := func(condJS string) error {
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-page.GetContext().Done():
|
|
return page.GetContext().Err()
|
|
case <-ticker.C:
|
|
res, err := page.Eval(condJS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.Value.Bool() {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Navigation
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("navigate", func(call goja.FunctionCall) goja.Value {
|
|
rawURL := argStr(call.Argument(0))
|
|
dbg("→ navigate " + rawURL)
|
|
must(validateNavigateURL(rawURL))
|
|
must(page.Navigate(rawURL))
|
|
must(page.WaitLoad())
|
|
dbg("✓ navigate " + rawURL)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// navigateToHistoryOffset navigates by history offset without waiting for
|
|
// a load event (avoids deadlock on cached pages).
|
|
navigateToHistoryOffset := func(offset int) error {
|
|
res, err := proto.PageGetNavigationHistory{}.Call(page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetIndex := res.CurrentIndex + offset
|
|
if targetIndex < 0 || targetIndex >= len(res.Entries) {
|
|
return fmt.Errorf("no history entry at offset %d", offset)
|
|
}
|
|
targetEntry := res.Entries[targetIndex]
|
|
targetURL := targetEntry.URL
|
|
navCmd := proto.PageNavigateToHistoryEntry{EntryID: targetEntry.ID}
|
|
if err := navCmd.Call(page); err != nil {
|
|
return err
|
|
}
|
|
deadline := time.Now().Add(30 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
info, err := page.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.URL == targetURL {
|
|
return nil
|
|
}
|
|
select {
|
|
case <-page.GetContext().Done():
|
|
return page.GetContext().Err()
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
}
|
|
return fmt.Errorf("timed out waiting for navigation to %s", targetURL)
|
|
}
|
|
|
|
pc.Set("navigateBack", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ navigateBack")
|
|
must(navigateToHistoryOffset(-1))
|
|
dbg("✓ navigateBack")
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("navigateForward", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ navigateForward")
|
|
must(navigateToHistoryOffset(+1))
|
|
dbg("✓ navigateForward")
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("reload", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ reload")
|
|
must(page.Reload())
|
|
dbg("✓ reload")
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("stop", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ stop")
|
|
must(proto.PageStopLoading{}.Call(page))
|
|
dbg("✓ stop")
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("location", func(call goja.FunctionCall) goja.Value {
|
|
info, err := page.Info()
|
|
must(err)
|
|
dbg("= location " + info.URL)
|
|
return vm.ToValue(info.URL)
|
|
})
|
|
|
|
pc.Set("title", func(call goja.FunctionCall) goja.Value {
|
|
info, err := page.Info()
|
|
must(err)
|
|
dbg("= title " + info.Title)
|
|
return vm.ToValue(info.Title)
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Waiting
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("waitVisible", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("… waitVisible " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.WaitVisible())
|
|
dbg("✓ waitVisible " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("waitReady", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("… waitReady " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.WaitVisible())
|
|
must(el.WaitEnabled())
|
|
dbg("✓ waitReady " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("waitEnabled", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("… waitEnabled " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.WaitEnabled())
|
|
dbg("✓ waitEnabled " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("waitSelected", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("… waitSelected " + sel)
|
|
must(pollUntil(fmt.Sprintf("() => { const el = document.querySelector(%q); return !!el && el.selected }", sel)))
|
|
dbg("✓ waitSelected " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("waitNotVisible", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("… waitNotVisible " + sel)
|
|
must(pollUntil(fmt.Sprintf("() => { const el = document.querySelector(%q); return !el || el.offsetParent === null }", sel)))
|
|
dbg("✓ waitNotVisible " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("waitNotPresent", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("… waitNotPresent " + sel)
|
|
must(pollUntil(fmt.Sprintf("() => !document.querySelector(%q)", sel)))
|
|
dbg("✓ waitNotPresent " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Mouse
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("click", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ click " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.Click(proto.InputMouseButtonLeft, 1))
|
|
dbg("✓ click " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("doubleClick", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ doubleClick " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.Click(proto.InputMouseButtonLeft, 2))
|
|
dbg("✓ doubleClick " + sel)
|
|
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}))
|
|
must(page.Mouse.Click(proto.InputMouseButtonLeft, 1))
|
|
dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("scrollIntoView", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ scrollIntoView " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.ScrollIntoView())
|
|
dbg("✓ scrollIntoView " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Keyboard
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("sendKeys", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
text := argStr(call.Argument(1))
|
|
dbg(fmt.Sprintf("→ sendKeys %s (%d chars)", sel, len(text)))
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.Focus())
|
|
must(page.InsertText(text))
|
|
dbg("✓ sendKeys " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// namedKeys maps CDP/browser key name strings to rod input.Key constants.
|
|
// For single-character keys the rune value is used directly as a fallback.
|
|
namedKeys := map[string]input.Key{
|
|
"Enter": input.Enter, "Return": input.Enter,
|
|
"Tab": input.Tab, "Escape": input.Escape, "Backspace": input.Backspace,
|
|
"Delete": input.Delete, "Insert": input.Insert,
|
|
"Home": input.Home, "End": input.End,
|
|
"PageUp": input.PageUp, "PageDown": input.PageDown,
|
|
"ArrowLeft": input.ArrowLeft, "ArrowRight": input.ArrowRight,
|
|
"ArrowUp": input.ArrowUp, "ArrowDown": input.ArrowDown,
|
|
" ": input.Space, "Space": input.Space,
|
|
"F1": input.F1, "F2": input.F2, "F3": input.F3, "F4": input.F4,
|
|
"F5": input.F5, "F6": input.F6, "F7": input.F7, "F8": input.F8,
|
|
"F9": input.F9, "F10": input.F10, "F11": input.F11, "F12": input.F12,
|
|
"ShiftLeft": input.ShiftLeft, "ShiftRight": input.ShiftRight,
|
|
"ControlLeft": input.ControlLeft, "ControlRight": input.ControlRight,
|
|
"AltLeft": input.AltLeft, "AltRight": input.AltRight,
|
|
}
|
|
|
|
pc.Set("keyEvent", func(call goja.FunctionCall) goja.Value {
|
|
key := argStr(call.Argument(0))
|
|
dbg("→ keyEvent " + key)
|
|
if k, ok := namedKeys[key]; ok {
|
|
must(page.Keyboard.Press(k))
|
|
} else {
|
|
runes := []rune(key)
|
|
if len(runes) > 0 {
|
|
must(page.Keyboard.Press(input.Key(runes[0])))
|
|
}
|
|
}
|
|
dbg("✓ keyEvent " + key)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Form
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("clear", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ clear " + sel)
|
|
script := fmt.Sprintf(`() => {
|
|
var el = document.querySelector(%q);
|
|
if (!el) return;
|
|
var nativeInput = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
|
|
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');
|
|
if (nativeInput && nativeInput.set) {
|
|
nativeInput.set.call(el, '');
|
|
} else {
|
|
el.value = '';
|
|
}
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}`, sel)
|
|
_, err := page.Eval(script)
|
|
must(err)
|
|
dbg("✓ clear " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("focus", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ focus " + sel)
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.Focus())
|
|
dbg("✓ focus " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("blur", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ blur " + sel)
|
|
_, err := page.Eval(fmt.Sprintf("() => { const el = document.querySelector(%q); if(el) el.blur() }", sel))
|
|
must(err)
|
|
dbg("✓ blur " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("submit", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("→ submit " + sel)
|
|
_, err := page.Eval(fmt.Sprintf("() => { const el = document.querySelector(%q); if(el) el.submit() }", sel))
|
|
must(err)
|
|
dbg("✓ submit " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("setValue", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
val := argStr(call.Argument(1))
|
|
dbg(fmt.Sprintf("→ setValue %s = %q", sel, val))
|
|
el, err := page.Element(sel)
|
|
must(err)
|
|
must(el.Input(val))
|
|
dbg("✓ setValue " + sel)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("getValue", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getValue " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
prop, err := el.Property("value")
|
|
must(err)
|
|
val := prop.Str()
|
|
dbg(fmt.Sprintf("= getValue %s %q", sel, val))
|
|
return vm.ToValue(val)
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// DOM reading
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("getText", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getText " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
text, err := el.Text()
|
|
must(err)
|
|
dbg(fmt.Sprintf("= getText %s %q", sel, text))
|
|
return vm.ToValue(text)
|
|
})
|
|
|
|
pc.Set("getTextContent", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getTextContent " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
res, err := el.Eval("() => this.textContent")
|
|
must(err)
|
|
text := res.Value.Str()
|
|
dbg(fmt.Sprintf("= getTextContent %s %q", sel, text))
|
|
return vm.ToValue(text)
|
|
})
|
|
|
|
pc.Set("getInnerHTML", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getInnerHTML " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
res, err := el.Eval("() => this.innerHTML")
|
|
must(err)
|
|
html := res.Value.Str()
|
|
dbg(fmt.Sprintf("= getInnerHTML %s (%d bytes)", sel, len(html)))
|
|
return vm.ToValue(html)
|
|
})
|
|
|
|
pc.Set("getOuterHTML", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getOuterHTML " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
html, err := el.HTML()
|
|
must(err)
|
|
dbg(fmt.Sprintf("= getOuterHTML %s (%d bytes)", sel, len(html)))
|
|
return vm.ToValue(html)
|
|
})
|
|
|
|
pc.Set("getAttribute", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
attr := argStr(call.Argument(1))
|
|
dbg(fmt.Sprintf("? getAttribute %s[%s]", sel, attr))
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
val, err := el.Attribute(attr)
|
|
must(err)
|
|
if val == nil {
|
|
dbg(fmt.Sprintf("= getAttribute %s[%s] null", sel, attr))
|
|
return goja.Null()
|
|
}
|
|
dbg(fmt.Sprintf("= getAttribute %s[%s] %q", sel, attr, *val))
|
|
return vm.ToValue(*val)
|
|
})
|
|
|
|
pc.Set("getAttributes", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getAttributes " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
res, err := rPage.Eval(fmt.Sprintf("() => { const el = document.querySelector(%q); if (!el) return {}; return Object.fromEntries([...el.attributes].map(a => [a.name, a.value])) }", sel))
|
|
must(err)
|
|
dbg(fmt.Sprintf("= getAttributes %s", sel))
|
|
return vm.ToValue(res.Value.Val())
|
|
})
|
|
|
|
pc.Set("setAttribute", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
attr := argStr(call.Argument(1))
|
|
val := argStr(call.Argument(2))
|
|
dbg(fmt.Sprintf("→ setAttribute %s[%s] = %q", sel, attr, val))
|
|
_, err := page.Eval(fmt.Sprintf("() => { const el = document.querySelector(%q); if(el) el.setAttribute(%q, %q) }", sel, attr, val))
|
|
must(err)
|
|
dbg(fmt.Sprintf("✓ setAttribute %s[%s]", sel, attr))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("removeAttribute", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
attr := argStr(call.Argument(1))
|
|
dbg(fmt.Sprintf("→ removeAttribute %s[%s]", sel, attr))
|
|
_, err := page.Eval(fmt.Sprintf("() => { const el = document.querySelector(%q); if(el) el.removeAttribute(%q) }", sel, attr))
|
|
must(err)
|
|
dbg(fmt.Sprintf("✓ removeAttribute %s[%s]", sel, attr))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("getJSAttribute", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
attr := argStr(call.Argument(1))
|
|
dbg(fmt.Sprintf("? getJSAttribute %s.%s", sel, attr))
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
prop, err := el.Property(attr)
|
|
must(err)
|
|
dbg(fmt.Sprintf("= getJSAttribute %s.%s %v", sel, attr, prop.Val()))
|
|
return vm.ToValue(prop.Val())
|
|
})
|
|
|
|
pc.Set("setJSAttribute", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
attr := argStr(call.Argument(1))
|
|
val := argStr(call.Argument(2))
|
|
dbg(fmt.Sprintf("→ setJSAttribute %s.%s = %q", sel, attr, val))
|
|
_, err := page.Eval(fmt.Sprintf("() => { const el = document.querySelector(%q); if(el) el[%q] = %q }", sel, attr, val))
|
|
must(err)
|
|
dbg(fmt.Sprintf("✓ setJSAttribute %s.%s", sel, attr))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("getNodeCount", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
dbg("? getNodeCount " + sel)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
els, err := rPage.Elements(sel)
|
|
if err != nil {
|
|
return vm.ToValue(0)
|
|
}
|
|
dbg(fmt.Sprintf("= getNodeCount %s %d", sel, len(els)))
|
|
return vm.ToValue(len(els))
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// JavaScript evaluation
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("evaluate", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ evaluate")
|
|
expr := argStr(call.Argument(0))
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
res, err := proto.RuntimeEvaluate{Expression: expr}.Call(rPage)
|
|
must(err)
|
|
result := res.Result.Value.Val()
|
|
dbg(fmt.Sprintf("= evaluate %v", result))
|
|
return vm.ToValue(result)
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Screenshots
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("screenshot", func(call goja.FunctionCall) goja.Value {
|
|
name := argStr(call.Argument(0))
|
|
dbg("→ screenshot " + name)
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
info, _ := rPage.Info()
|
|
buf, err := rPage.Screenshot(true, nil)
|
|
if err != nil {
|
|
if emitter != nil {
|
|
emitter.log(fmt.Sprintf("[screenshot] %s: %s", name, err))
|
|
}
|
|
return goja.Undefined()
|
|
}
|
|
pageURL := ""
|
|
if info != nil {
|
|
pageURL = info.URL
|
|
}
|
|
if emitter != nil {
|
|
emitter.screenshot(name, buf, pageURL)
|
|
}
|
|
dbg("✓ screenshot " + name)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("screenshotElement", func(call goja.FunctionCall) goja.Value {
|
|
sel := argStr(call.Argument(0))
|
|
name := argStr(call.Argument(1))
|
|
dbg(fmt.Sprintf("→ screenshotElement %s as %s", sel, name))
|
|
rPage, rCancel := readPage()
|
|
defer rCancel()
|
|
info, _ := rPage.Info()
|
|
el, err := rPage.Element(sel)
|
|
must(err)
|
|
must(el.WaitVisible())
|
|
buf, err := el.Screenshot("", 0)
|
|
if err != nil {
|
|
if emitter != nil {
|
|
emitter.log(fmt.Sprintf("[screenshot] %s: %s", name, err))
|
|
}
|
|
return goja.Undefined()
|
|
}
|
|
pageURL := ""
|
|
if info != nil {
|
|
pageURL = info.URL
|
|
}
|
|
if emitter != nil {
|
|
emitter.screenshot(name, buf, pageURL)
|
|
}
|
|
dbg("✓ screenshotElement " + name)
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Viewport & emulation
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("setViewport", func(call goja.FunctionCall) goja.Value {
|
|
w := call.Argument(0).ToInteger()
|
|
h := call.Argument(1).ToInteger()
|
|
dbg(fmt.Sprintf("→ setViewport %dx%d", w, h))
|
|
must(proto.EmulationSetDeviceMetricsOverride{
|
|
Width: int(w), Height: int(h), DeviceScaleFactor: 1,
|
|
}.Call(page))
|
|
dbg(fmt.Sprintf("✓ setViewport %dx%d", w, h))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("setViewportMobile", func(call goja.FunctionCall) goja.Value {
|
|
w := call.Argument(0).ToInteger()
|
|
h := call.Argument(1).ToInteger()
|
|
dbg(fmt.Sprintf("→ setViewportMobile %dx%d", w, h))
|
|
must(proto.EmulationSetDeviceMetricsOverride{
|
|
Width: int(w), Height: int(h), DeviceScaleFactor: 1,
|
|
Mobile: true,
|
|
}.Call(page))
|
|
must(proto.EmulationSetTouchEmulationEnabled{Enabled: true}.Call(page))
|
|
dbg(fmt.Sprintf("✓ setViewportMobile %dx%d", w, h))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("resetViewport", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ resetViewport")
|
|
must(proto.EmulationClearDeviceMetricsOverride{}.Call(page))
|
|
dbg("✓ resetViewport")
|
|
return goja.Undefined()
|
|
})
|
|
|
|
pc.Set("setUserAgent", func(call goja.FunctionCall) goja.Value {
|
|
ua := argStr(call.Argument(0))
|
|
dbg("→ setUserAgent " + ua)
|
|
must(proto.EmulationSetUserAgentOverride{UserAgent: ua}.Call(page))
|
|
dbg("✓ setUserAgent")
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Utility
|
|
// -------------------------------------------------------------------------
|
|
|
|
pc.Set("sleep", func(call goja.FunctionCall) goja.Value {
|
|
ms := call.Argument(0).ToInteger()
|
|
dbg(fmt.Sprintf("→ sleep %dms", ms))
|
|
select {
|
|
case <-page.GetContext().Done():
|
|
must(page.GetContext().Err())
|
|
case <-time.After(time.Duration(ms) * time.Millisecond):
|
|
}
|
|
dbg(fmt.Sprintf("✓ sleep %dms", ms))
|
|
return goja.Undefined()
|
|
})
|
|
|
|
// disableFidoUI enables the CDP WebAuthn virtual authenticator environment.
|
|
// In this mode Chrome intercepts WebAuthn/FIDO requests via CDP instead of
|
|
// showing the native "Passkeys & Security Keys" browser dialog, so DOM
|
|
// interactions remain possible while on the FIDO page.
|
|
pc.Set("disableFidoUI", func(call goja.FunctionCall) goja.Value {
|
|
dbg("→ disableFidoUI")
|
|
must(proto.WebAuthnEnable{}.Call(page))
|
|
dbg("✓ disableFidoUI")
|
|
return goja.Undefined()
|
|
})
|
|
}
|