Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(headless): add ActionWaitDialog type #5545

Merged
merged 10 commits into from
Sep 2, 2024
9 changes: 9 additions & 0 deletions pkg/protocols/headless/engine/action_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import (
"strings"

"github.com/invopop/jsonschema"
mapsutil "github.com/projectdiscovery/utils/maps"
)

// ActionType defines the action type for a browser action
type ActionType int8

// ActionData stores the action output data
type ActionData = mapsutil.Map[string, any]

// Types to be executed by the user.
// name:ActionType
const (
Expand Down Expand Up @@ -68,6 +72,9 @@ const (
// ActionWaitEvent waits for a specific event.
// name:waitevent
ActionWaitEvent
// ActionWaitDialog waits for JavaScript dialog (alert, confirm, prompt, or onbeforeunload).
// name:dialog
ActionWaitDialog
// ActionKeyboard performs a keyboard action event on a page.
// name:keyboard
ActionKeyboard
Expand Down Expand Up @@ -104,6 +111,7 @@ var ActionStringToAction = map[string]ActionType{
"deleteheader": ActionDeleteHeader,
"setbody": ActionSetBody,
"waitevent": ActionWaitEvent,
"waitdialog": ActionWaitDialog,
"keyboard": ActionKeyboard,
"debug": ActionDebug,
"sleep": ActionSleep,
Expand All @@ -130,6 +138,7 @@ var ActionToActionString = map[ActionType]string{
ActionDeleteHeader: "deleteheader",
ActionSetBody: "setbody",
ActionWaitEvent: "waitevent",
ActionWaitDialog: "waitdialog",
ActionKeyboard: "keyboard",
ActionDebug: "debug",
ActionSleep: "sleep",
Expand Down
2 changes: 1 addition & 1 deletion pkg/protocols/headless/engine/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type Options struct {
}

// Run runs a list of actions by creating a new page in the browser.
func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (map[string]string, *Page, error) {
func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (ActionData, *Page, error) {
page, err := i.engine.Page(proto.TargetCreateTarget{})
if err != nil {
return nil, nil, err
Expand Down
87 changes: 63 additions & 24 deletions pkg/protocols/headless/engine/page_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ const (
)

// ExecuteActions executes a list of actions on a page.
func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, variables map[string]interface{}) (outData map[string]string, err error) {
outData = make(map[string]string)
func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, variables map[string]interface{}) (outData ActionData, err error) {
outData = make(ActionData)
// waitFuncs are function that needs to be executed after navigation
// typically used for waitEvent
waitFuncs := make([]func() error, 0)
Expand Down Expand Up @@ -100,6 +100,8 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, var
if waitFunc != nil {
waitFuncs = append(waitFuncs, waitFunc)
}
case ActionWaitDialog:
err = p.HandleDialog(act, outData)
case ActionFilesInput:
if p.options.Options.AllowLocalFileAccess {
err = p.FilesInput(act, outData)
Expand Down Expand Up @@ -142,7 +144,7 @@ type rule struct {
}

// WaitVisible waits until an element appears.
func (p *Page) WaitVisible(act *Action, out map[string]string) error {
func (p *Page) WaitVisible(act *Action, out ActionData) error {
timeout, err := getTimeout(p, act)
if err != nil {
return errors.Wrap(err, "Wrong timeout given")
Expand Down Expand Up @@ -217,7 +219,7 @@ func geTimeParameter(p *Page, act *Action, parameterName string, defaultValue ti
}

// ActionAddHeader executes a AddHeader action.
func (p *Page) ActionAddHeader(act *Action, out map[string]string) error {
func (p *Page) ActionAddHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")

args := make(map[string]string)
Expand All @@ -228,7 +230,7 @@ func (p *Page) ActionAddHeader(act *Action, out map[string]string) error {
}

// ActionSetHeader executes a SetHeader action.
func (p *Page) ActionSetHeader(act *Action, out map[string]string) error {
func (p *Page) ActionSetHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")

args := make(map[string]string)
Expand All @@ -239,7 +241,7 @@ func (p *Page) ActionSetHeader(act *Action, out map[string]string) error {
}

// ActionDeleteHeader executes a DeleteHeader action.
func (p *Page) ActionDeleteHeader(act *Action, out map[string]string) error {
func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")

args := make(map[string]string)
Expand All @@ -249,7 +251,7 @@ func (p *Page) ActionDeleteHeader(act *Action, out map[string]string) error {
}

// ActionSetBody executes a SetBody action.
func (p *Page) ActionSetBody(act *Action, out map[string]string) error {
func (p *Page) ActionSetBody(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")

args := make(map[string]string)
Expand All @@ -259,7 +261,7 @@ func (p *Page) ActionSetBody(act *Action, out map[string]string) error {
}

// ActionSetMethod executes an SetMethod action.
func (p *Page) ActionSetMethod(act *Action, out map[string]string) error {
func (p *Page) ActionSetMethod(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part")

args := make(map[string]string)
Expand All @@ -269,7 +271,7 @@ func (p *Page) ActionSetMethod(act *Action, out map[string]string) error {
}

// NavigateURL executes an ActionLoadURL actions loading a URL for the page.
func (p *Page) NavigateURL(action *Action, out map[string]string, allvars map[string]interface{}) error {
func (p *Page) NavigateURL(action *Action, out ActionData, allvars map[string]interface{}) error {
// input <- is input url from cli
// target <- is the url from template (ex: {{BaseURL}}/test)
input, err := urlutil.Parse(p.input.MetaInput.Input)
Expand Down Expand Up @@ -325,7 +327,7 @@ func (p *Page) NavigateURL(action *Action, out map[string]string, allvars map[st
}

// RunScript runs a script on the loaded page
func (p *Page) RunScript(action *Action, out map[string]string) error {
func (p *Page) RunScript(action *Action, out ActionData) error {
code := p.getActionArgWithDefaultValues(action, "code")
if code == "" {
return errinvalidArguments
Expand All @@ -346,7 +348,7 @@ func (p *Page) RunScript(action *Action, out map[string]string) error {
}

// ClickElement executes click actions for an element.
func (p *Page) ClickElement(act *Action, out map[string]string) error {
func (p *Page) ClickElement(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data)
if err != nil {
return errors.Wrap(err, errCouldNotGetElement)
Expand All @@ -361,12 +363,12 @@ func (p *Page) ClickElement(act *Action, out map[string]string) error {
}

// KeyboardAction executes a keyboard action on the page.
func (p *Page) KeyboardAction(act *Action, out map[string]string) error {
func (p *Page) KeyboardAction(act *Action, out ActionData) error {
return p.page.Keyboard.Type([]input.Key(p.getActionArgWithDefaultValues(act, "keys"))...)
}

// RightClickElement executes right click actions for an element.
func (p *Page) RightClickElement(act *Action, out map[string]string) error {
func (p *Page) RightClickElement(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data)
if err != nil {
return errors.Wrap(err, errCouldNotGetElement)
Expand All @@ -381,7 +383,7 @@ func (p *Page) RightClickElement(act *Action, out map[string]string) error {
}

// Screenshot executes screenshot action on a page
func (p *Page) Screenshot(act *Action, out map[string]string) error {
func (p *Page) Screenshot(act *Action, out ActionData) error {
to := p.getActionArgWithDefaultValues(act, "to")
if to == "" {
to = ksuid.New().String()
Expand Down Expand Up @@ -444,7 +446,7 @@ func (p *Page) Screenshot(act *Action, out map[string]string) error {
}

// InputElement executes input element actions for an element.
func (p *Page) InputElement(act *Action, out map[string]string) error {
func (p *Page) InputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value")
if value == "" {
return errinvalidArguments
Expand All @@ -463,7 +465,7 @@ func (p *Page) InputElement(act *Action, out map[string]string) error {
}

// TimeInputElement executes time input on an element
func (p *Page) TimeInputElement(act *Action, out map[string]string) error {
func (p *Page) TimeInputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value")
if value == "" {
return errinvalidArguments
Expand All @@ -486,7 +488,7 @@ func (p *Page) TimeInputElement(act *Action, out map[string]string) error {
}

// SelectInputElement executes select input statement action on a element
func (p *Page) SelectInputElement(act *Action, out map[string]string) error {
func (p *Page) SelectInputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value")
if value == "" {
return errinvalidArguments
Expand All @@ -511,7 +513,7 @@ func (p *Page) SelectInputElement(act *Action, out map[string]string) error {
}

// WaitLoad waits for the page to load
func (p *Page) WaitLoad(act *Action, out map[string]string) error {
func (p *Page) WaitLoad(act *Action, out ActionData) error {
p.page.Timeout(2 * time.Second).WaitNavigation(proto.PageLifecycleEventNameFirstMeaningfulPaint)()

// Wait for the window.onload event and also wait for the network requests
Expand All @@ -525,7 +527,7 @@ func (p *Page) WaitLoad(act *Action, out map[string]string) error {
}

// GetResource gets a resource from an element from page.
func (p *Page) GetResource(act *Action, out map[string]string) error {
func (p *Page) GetResource(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data)
if err != nil {
return errors.Wrap(err, errCouldNotGetElement)
Expand All @@ -541,7 +543,7 @@ func (p *Page) GetResource(act *Action, out map[string]string) error {
}

// FilesInput acts with a file input element on page
func (p *Page) FilesInput(act *Action, out map[string]string) error {
func (p *Page) FilesInput(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data)
if err != nil {
return errors.Wrap(err, errCouldNotGetElement)
Expand All @@ -558,7 +560,7 @@ func (p *Page) FilesInput(act *Action, out map[string]string) error {
}

// ExtractElement extracts from an element on the page.
func (p *Page) ExtractElement(act *Action, out map[string]string) error {
func (p *Page) ExtractElement(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data)
if err != nil {
return errors.Wrap(err, errCouldNotGetElement)
Expand Down Expand Up @@ -592,7 +594,7 @@ func (p *Page) ExtractElement(act *Action, out map[string]string) error {
}

// WaitEvent waits for an event to happen on the page.
func (p *Page) WaitEvent(act *Action, out map[string]string) (func() error, error) {
func (p *Page) WaitEvent(act *Action, out ActionData) (func() error, error) {
event := p.getActionArgWithDefaultValues(act, "event")
if event == "" {
return nil, errors.New("event not recognized")
Expand Down Expand Up @@ -630,6 +632,43 @@ func (p *Page) WaitEvent(act *Action, out map[string]string) (func() error, erro
return waitFunc, nil
}

// HandleDialog handles JavaScript dialog (alert, confirm, prompt, or onbeforeunload).
func (p *Page) HandleDialog(act *Action, out ActionData) error {
maxDuration := 10 * time.Second

if dur := p.getActionArgWithDefaultValues(act, "max-duration"); dur != "" {
var err error

maxDuration, err = time.ParseDuration(dur)
if err != nil {
return errorutil.NewWithErr(err).Msgf("could not parse max-duration")
}
}

ctx, cancel := context.WithTimeout(context.Background(), maxDuration)
defer cancel()

wait, handle := p.page.HandleDialog()
fn := func() (*proto.PageJavascriptDialogOpening, error) {
dialog := wait()
err := handle(&proto.PageHandleJavaScriptDialog{
Accept: true,
PromptText: "",
})

return dialog, err
}

dialog, err := contextutil.ExecFuncWithTwoReturns(ctx, fn)
if err == nil && act.Name != "" {
out[act.Name] = true
out[act.Name+"_type"] = string(dialog.Type)
out[act.Name+"_message"] = dialog.Message
}

return nil
}

// pageElementBy returns a page element from a variety of inputs.
//
// Supported values for by: r -> selector & regex, x -> xpath, js -> eval js,
Expand Down Expand Up @@ -664,14 +703,14 @@ func (p *Page) pageElementBy(data map[string]string) (*rod.Element, error) {
}

// DebugAction enables debug action on a page.
func (p *Page) DebugAction(act *Action, out map[string]string) error {
func (p *Page) DebugAction(act *Action, out ActionData) error {
p.instance.browser.engine.SlowMotion(5 * time.Second)
p.instance.browser.engine.Trace(true)
return nil
}

// SleepAction sleeps on the page for a specified duration
func (p *Page) SleepAction(act *Action, out map[string]string) error {
func (p *Page) SleepAction(act *Action, out ActionData) error {
seconds := act.Data["duration"]
if seconds == "" {
seconds = "5"
Expand Down
Loading
Loading