Add/Expose Headless action that waits until an element appears #1096

* add timeout and pollTime options
dev
forgedhallpass 2021-10-13 20:08:10 +03:00
parent 88ee74d68a
commit c0ef419048
2 changed files with 108 additions and 58 deletions

View File

@ -1,6 +1,7 @@
package engine package engine
import ( import (
"context"
"io/ioutil" "io/ioutil"
"net" "net"
"net/url" "net/url"
@ -11,6 +12,7 @@ import (
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto" "github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/segmentio/ksuid" "github.com/segmentio/ksuid"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
@ -46,7 +48,7 @@ func (p *Page) ExecuteActions(baseURL *url.URL, actions []*Action) (map[string]s
case ActionGetResource: case ActionGetResource:
err = p.GetResource(act, outData) err = p.GetResource(act, outData)
case ActionExtract: case ActionExtract:
err = p.SelectInputElement(act, outData) err = p.ExtractElement(act, outData)
case ActionWaitEvent: case ActionWaitEvent:
err = p.WaitEvent(act, outData) err = p.WaitEvent(act, outData)
case ActionFilesInput: case ActionFilesInput:
@ -85,18 +87,83 @@ type requestRule struct {
Args map[string]string Args map[string]string
} }
const elementDidNotAppearMessage = "Element did not appear in the given amount of time"
// WaitVisible waits until an element appears. // WaitVisible waits until an element appears.
func (p *Page) WaitVisible(act *Action, out map[string]string) error { func (p *Page) WaitVisible(act *Action, out map[string]string) error {
element, err := p.pageElementBy(act.Data) timeout, err := getTimeout(act)
if err != nil { if err != nil {
return errors.Wrap(err, "could not find element") return errors.Wrap(err, "Wrong timeout given")
} }
if err = element.WaitVisible(); err != nil {
return errors.Wrap(err, "could not wait element") pollTime, err := getPollTime(act)
if err != nil {
return errors.Wrap(err, "Wrong polling time given")
} }
element, _ := p.Sleeper(pollTime, timeout).
Timeout(timeout).
pageElementBy(act.Data)
if element != nil {
if err := element.WaitVisible(); err != nil {
return errors.Wrap(err, elementDidNotAppearMessage)
}
} else {
return errors.New(elementDidNotAppearMessage)
}
return nil return nil
} }
func (p *Page) Sleeper(pollTimeout, timeout time.Duration) *Page {
page := *p
page.page = page.Page().Sleeper(func() utils.Sleeper {
return createBackOffSleeper(pollTimeout, timeout)
})
return &page
}
func (p *Page) Timeout(timeout time.Duration) *Page {
page := *p
page.page = page.Page().Timeout(timeout)
return &page
}
func createBackOffSleeper(pollTimeout, timeout time.Duration) utils.Sleeper {
backoffSleeper := utils.BackoffSleeper(pollTimeout, timeout, func(duration time.Duration) time.Duration {
return duration
})
return func(ctx context.Context) error {
if ctx.Err() != nil {
return ctx.Err()
}
return backoffSleeper(ctx)
}
}
func getTimeout(act *Action) (time.Duration, error) {
return geTimeParameter(act, "timeout", 3, time.Second)
}
func getPollTime(act *Action) (time.Duration, error) {
return geTimeParameter(act, "pollTime", 100, time.Millisecond)
}
func geTimeParameter(act *Action, parameterName string, defaultValue time.Duration, duration time.Duration) (time.Duration, error) {
pollTimeString := act.GetArg(parameterName)
if pollTimeString == "" {
return defaultValue * duration, nil
}
timeout, err := strconv.Atoi(pollTimeString)
if err != nil {
return time.Duration(0), err
}
return time.Duration(timeout) * duration, nil
}
// ActionAddHeader executes a AddHeader action. // ActionAddHeader executes a AddHeader action.
func (p *Page) ActionAddHeader(act *Action, out map[string]string /*TODO review unused parameter*/) error { func (p *Page) ActionAddHeader(act *Action, out map[string]string /*TODO review unused parameter*/) error {
in := act.GetArg("part") in := act.GetArg("part")

View File

@ -9,9 +9,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/projectdiscovery/nuclei/v2/pkg/types"
"github.com/stretchr/testify/require"
) )
func TestActionNavigate(t *testing.T) { func TestActionNavigate(t *testing.T) {
@ -281,17 +282,24 @@ func TestActionHeadersChange(t *testing.T) {
func TestActionWaitVisible(t *testing.T) { func TestActionWaitVisible(t *testing.T) {
t.Run("wait for an element being visible", func(t *testing.T) { t.Run("wait for an element being visible", func(t *testing.T) {
_ = protocolstate.Init(&types.Options{}) testWaitVisible(t, 2*time.Second, func(page *Page, err error) {
require.Nil(t, err, "could not run page actions")
browser, err := New(&types.Options{ShowBrowser: false}) page.Page().MustElement("button").MustVisible()
require.Nil(t, err, "could not create browser") page.Close()
defer browser.Close() })
})
instance, err := browser.NewInstance() t.Run("timeout because of element not visible", func(t *testing.T) {
require.Nil(t, err, "could not create browser instance") testWaitVisible(t, time.Second/2, func(page *Page, err error) {
require.Error(t, err)
require.Contains(t, err.Error(), "Element did not appear in the given amount of time")
})
})
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { func testWaitVisible(t *testing.T, timeout time.Duration, assert func(page *Page, err error)) {
fmt.Fprintln(w, ` response := `
<html> <html>
<head> <head>
<title>Nuclei Test Page</title> <title>Nuclei Test Page</title>
@ -300,54 +308,29 @@ func TestActionWaitVisible(t *testing.T) {
<script> <script>
setTimeout(() => document.querySelector('#test').style.display = '', 1000); setTimeout(() => document.querySelector('#test').style.display = '', 1000);
</script> </script>
</html>`) </html>`
}))
defer ts.Close()
parsed, err := url.Parse(ts.URL) _ = protocolstate.Init(&types.Options{})
require.Nil(t, err, "could not parse URL")
actions := []*Action{ browser, err := New(&types.Options{ShowBrowser: false})
{ActionType: "navigate", Data: map[string]string{"url": "{{BaseURL}}"}}, require.Nil(t, err, "could not create browser")
{ActionType: "waitvisible", Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}}, defer browser.Close()
}
_, page, err := instance.Run(parsed, actions, 20*time.Second)
require.Nil(t, err, "could not run page actions")
defer page.Close()
page.Page().MustElement("button").MustVisible() instance, err := browser.NewInstance()
}) require.Nil(t, err, "could not create browser instance")
t.Run("timeout because of element not visible", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = protocolstate.Init(&types.Options{}) fmt.Fprintln(w, response)
}))
defer ts.Close()
browser, err := New(&types.Options{ShowBrowser: false}) parsed, err := url.Parse(ts.URL)
require.Nil(t, err, "could not create browser") require.Nil(t, err, "could not parse URL")
defer browser.Close()
instance, err := browser.NewInstance() actions := []*Action{
require.Nil(t, err, "could not create browser instance") {ActionType: "navigate", Data: map[string]string{"url": "{{BaseURL}}"}},
{ActionType: "waitvisible", Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}},
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { }
fmt.Fprintln(w, ` _, page, err := instance.Run(parsed, actions, timeout)
<html> assert(page, err)
<head>
<title>Nuclei Test Page</title>
</head>
<button style="display:none" id="test">Wait for me!</button>
</html>`)
}))
defer ts.Close()
parsed, err := url.Parse(ts.URL)
require.Nil(t, err, "could not parse URL")
actions := []*Action{
{ActionType: "navigate", Data: map[string]string{"url": "{{BaseURL}}"}},
{ActionType: "waitvisible", Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}},
}
_, _, err = instance.Run(parsed, actions, 2*time.Second)
require.Error(t, err)
require.Contains(t, err.Error(), "could not wait element")
})
} }