mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 02:08:33 -10:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
649697c9af | ||
|
|
d2b4564681 | ||
|
|
f438963e83 | ||
|
|
aaa6827409 | ||
|
|
5915f21f50 | ||
|
|
b76d8de501 | ||
|
|
bfff4f0d2c | ||
|
|
f238298505 | ||
|
|
108c8e28c1 | ||
|
|
342994fe2f | ||
|
|
7211cb35fb | ||
|
|
4a2c5c2be6 | ||
|
|
2ae0db918a | ||
|
|
ef4eb1dcf2 | ||
|
|
ef4ad4f4c7 | ||
|
|
f8a9087ccf | ||
|
|
857cc67702 |
No files matched your search
@@ -102,7 +102,7 @@ require (
|
||||
github.com/projectdiscovery/tlsx v1.4.1
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.10-0.20261015084917-a599cac78c99
|
||||
github.com/projectdiscovery/utils v0.11.10-0.20261019085724-c5c64e928d17
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.4
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
|
||||
@@ -1003,8 +1003,8 @@ github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLv
|
||||
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
|
||||
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
|
||||
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
|
||||
github.com/projectdiscovery/utils v0.11.10-0.20261015084917-a599cac78c99 h1:1RG4XinX0GqkAqIBxG92ldRWCvU6yZn9YdSFh2EiH2o=
|
||||
github.com/projectdiscovery/utils v0.11.10-0.20261015084917-a599cac78c99/go.mod h1:d3BcbsHhuxW9T3jpvrgzwshvqhaL9qPSkoi+3h/DY8E=
|
||||
github.com/projectdiscovery/utils v0.11.10-0.20261019085724-c5c64e928d17 h1:aSNbcIUA6wZyHaGdZjFzmZ5irbMMg5Yc5WmuXdmnvyI=
|
||||
github.com/projectdiscovery/utils v0.11.10-0.20261019085724-c5c64e928d17/go.mod h1:d3BcbsHhuxW9T3jpvrgzwshvqhaL9qPSkoi+3h/DY8E=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.4 h1:yO+xel+C0d/vOFWcUKVZRpIDAfcEmsjpVDjQ0jifAUw=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.4/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/utils/env"
|
||||
httputil "github.com/projectdiscovery/utils/http"
|
||||
@@ -16,6 +17,7 @@ var (
|
||||
MaxBytesBufferAllocOnLowMemory = env.GetEnvOrDefault("MEMGUARDIAN_ALLOC", 0)
|
||||
memTimer *time.Ticker
|
||||
cancelFunc context.CancelFunc
|
||||
memWatcherDone chan struct{}
|
||||
muGlobalChange sync.Mutex
|
||||
)
|
||||
|
||||
@@ -25,12 +27,18 @@ func StartActiveMemGuardian(ctx context.Context) {
|
||||
if memguardian.DefaultMemGuardian == nil || memTimer != nil {
|
||||
return
|
||||
}
|
||||
if err := refreshMemoryWarning(); err != nil {
|
||||
gologger.Warning().Msgf("could not check memory pressure: %s", err)
|
||||
}
|
||||
|
||||
memTimer = time.NewTicker(memguardian.DefaultInterval)
|
||||
ctx, cancelFunc = context.WithCancel(ctx)
|
||||
memWatcherDone = make(chan struct{})
|
||||
|
||||
ticker := memTimer
|
||||
done := memWatcherDone
|
||||
go func(t *time.Ticker) {
|
||||
defer close(done)
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
@@ -39,6 +47,9 @@ func StartActiveMemGuardian(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if err := refreshMemoryWarning(); err != nil {
|
||||
gologger.Warning().Msgf("could not check memory pressure: %s", err)
|
||||
}
|
||||
if IsLowOnMemory() {
|
||||
_ = GlobalGuardBytesBufferAlloc()
|
||||
} else {
|
||||
@@ -49,6 +60,24 @@ func StartActiveMemGuardian(ctx context.Context) {
|
||||
}(ticker)
|
||||
}
|
||||
|
||||
func refreshMemoryWarning() error {
|
||||
usedRatio, usedBytes, err := memguardian.UsedRam()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
warning := usedRatio >= memguardian.DefaultMaxUsedRamRatio
|
||||
if maxRam := env.GetEnvOrDefault(memguardian.MemGuardianMaxUsedMemoryENV, ""); maxRam != "" {
|
||||
maxBytes, err := units.FromHumanSize(maxRam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
warning = warning || maxBytes > 0 && usedBytes >= uint64(maxBytes)
|
||||
}
|
||||
memguardian.DefaultMemGuardian.Warning.Store(warning)
|
||||
return nil
|
||||
}
|
||||
|
||||
func StopActiveMemGuardian() {
|
||||
muGlobalChange.Lock()
|
||||
defer muGlobalChange.Unlock()
|
||||
@@ -61,6 +90,10 @@ func StopActiveMemGuardian() {
|
||||
cancelFunc()
|
||||
cancelFunc = nil
|
||||
}
|
||||
if memWatcherDone != nil {
|
||||
<-memWatcherDone
|
||||
memWatcherDone = nil
|
||||
}
|
||||
if memTimer != nil {
|
||||
memTimer.Stop()
|
||||
memTimer = nil
|
||||
@@ -77,7 +110,7 @@ func IsLowOnMemory() bool {
|
||||
// GuardThreads on caller
|
||||
func GuardThreadsOrDefault(current int) int {
|
||||
if MaxThreadsOnLowMemory > 0 {
|
||||
return MaxThreadsOnLowMemory
|
||||
return min(current, MaxThreadsOnLowMemory)
|
||||
}
|
||||
|
||||
fraction := int(current / 5)
|
||||
|
||||
@@ -127,3 +127,45 @@ func TestMemGuardianReset(t *testing.T) {
|
||||
StopActiveMemGuardian()
|
||||
time.Sleep(10 * time.Millisecond) // Allow cleanup
|
||||
}
|
||||
|
||||
func TestActiveMemGuardianUpdatesLowMemoryWarning(t *testing.T) {
|
||||
StopActiveMemGuardian()
|
||||
t.Setenv(memguardian.MemGuardianMaxUsedMemoryENV, "")
|
||||
original := memguardian.DefaultMemGuardian
|
||||
originalInterval := memguardian.DefaultInterval
|
||||
originalRatio := memguardian.DefaultMaxUsedRamRatio
|
||||
memguardian.DefaultInterval = 5 * time.Millisecond
|
||||
memguardian.DefaultMaxUsedRamRatio = 1
|
||||
guardian, err := memguardian.New(
|
||||
memguardian.WitInterval(memguardian.DefaultInterval),
|
||||
memguardian.WithMaxRamRatioWarning(1),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
memguardian.DefaultMemGuardian = guardian
|
||||
t.Cleanup(func() {
|
||||
StopActiveMemGuardian()
|
||||
guardian.Close()
|
||||
memguardian.DefaultMemGuardian = original
|
||||
memguardian.DefaultInterval = originalInterval
|
||||
memguardian.DefaultMaxUsedRamRatio = originalRatio
|
||||
})
|
||||
|
||||
StartActiveMemGuardian(context.Background())
|
||||
require.Eventually(t, IsLowOnMemory, 100*time.Millisecond, 5*time.Millisecond)
|
||||
StopActiveMemGuardian()
|
||||
memguardian.DefaultMaxUsedRamRatio = 100
|
||||
require.NoError(t, refreshMemoryWarning())
|
||||
require.False(t, IsLowOnMemory())
|
||||
t.Setenv(memguardian.MemGuardianMaxUsedMemoryENV, "0")
|
||||
require.NoError(t, refreshMemoryWarning())
|
||||
require.False(t, IsLowOnMemory())
|
||||
}
|
||||
|
||||
func TestGuardThreadsNeverIncreasesConcurrency(t *testing.T) {
|
||||
previous := MaxThreadsOnLowMemory
|
||||
MaxThreadsOnLowMemory = 5
|
||||
t.Cleanup(func() { MaxThreadsOnLowMemory = previous })
|
||||
|
||||
require.Equal(t, 2, GuardThreadsOrDefault(2))
|
||||
require.Equal(t, 5, GuardThreadsOrDefault(25))
|
||||
}
|
||||
@@ -52,9 +52,10 @@ type HistoryData struct {
|
||||
|
||||
// Options contains additional configuration options for the browser instance
|
||||
type Options struct {
|
||||
Timeout time.Duration
|
||||
DisableCookie bool
|
||||
Options *types.Options
|
||||
Timeout time.Duration
|
||||
DisableCookie bool
|
||||
Options *types.Options
|
||||
OnActionTiming func(ActionType, time.Duration)
|
||||
}
|
||||
|
||||
// Run runs a list of actions by creating a new page in the browser.
|
||||
|
||||
@@ -52,6 +52,7 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (ou
|
||||
|
||||
type deferredWaitFunc struct {
|
||||
actionIndex int
|
||||
actionType ActionType
|
||||
run func() error
|
||||
}
|
||||
|
||||
@@ -76,6 +77,7 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (ou
|
||||
actionIndex := len(p.ActionDurations)
|
||||
timeStart := time.Now()
|
||||
var deferredWaitDuration time.Duration
|
||||
deferredWaitFailed := false
|
||||
switch act.ActionType.ActionType {
|
||||
case ActionNavigate:
|
||||
err = p.NavigateURL(act, outData)
|
||||
@@ -84,18 +86,24 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (ou
|
||||
for _, waitFunc := range waitFuncs {
|
||||
if waitFunc.run != nil {
|
||||
waitStart := time.Now()
|
||||
if err := waitFunc.run(); err != nil {
|
||||
waitDuration := time.Since(waitStart)
|
||||
addActionDuration(p.ActionDurations, waitFunc.actionIndex, waitDuration)
|
||||
return nil, errkit.Wrap(err, "error occurred while executing waitFunc")
|
||||
}
|
||||
waitErr := waitFunc.run()
|
||||
waitDuration := time.Since(waitStart)
|
||||
addActionDuration(p.ActionDurations, waitFunc.actionIndex, waitDuration)
|
||||
if p.options.OnActionTiming != nil {
|
||||
p.options.OnActionTiming(waitFunc.actionType, waitDuration)
|
||||
}
|
||||
deferredWaitDuration += waitDuration
|
||||
if waitErr != nil {
|
||||
err = waitErr
|
||||
deferredWaitFailed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.lastActionNavigate = act
|
||||
if err == nil {
|
||||
p.lastActionNavigate = act
|
||||
}
|
||||
}
|
||||
case ActionScript:
|
||||
err = p.RunScript(act, outData)
|
||||
@@ -141,7 +149,7 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (ou
|
||||
var waitFunc func() error
|
||||
waitFunc, err = p.WaitEvent(act, outData)
|
||||
if waitFunc != nil {
|
||||
waitFuncs = append(waitFuncs, deferredWaitFunc{actionIndex: actionIndex, run: waitFunc})
|
||||
waitFuncs = append(waitFuncs, deferredWaitFunc{actionIndex: actionIndex, actionType: act.ActionType.ActionType, run: waitFunc})
|
||||
}
|
||||
case ActionWaitDialog:
|
||||
err = p.HandleDialog(act, outData)
|
||||
@@ -177,11 +185,20 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (ou
|
||||
default:
|
||||
executed = false
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error occurred executing action")
|
||||
}
|
||||
if executed {
|
||||
p.ActionDurations = append(p.ActionDurations, max(time.Since(timeStart)-deferredWaitDuration, 0))
|
||||
duration := max(time.Since(timeStart)-deferredWaitDuration, 0)
|
||||
if p.options.OnActionTiming != nil {
|
||||
p.options.OnActionTiming(act.ActionType.ActionType, duration)
|
||||
}
|
||||
if err == nil {
|
||||
p.ActionDurations = append(p.ActionDurations, duration)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if deferredWaitFailed {
|
||||
return nil, errkit.Wrap(err, "error occurred while executing waitFunc")
|
||||
}
|
||||
return nil, errors.Wrap(err, "error occurred executing action")
|
||||
}
|
||||
}
|
||||
return outData, nil
|
||||
@@ -230,7 +247,9 @@ func (p *Page) WaitVisible(act *Action, out ActionData) error {
|
||||
}
|
||||
|
||||
func (p *Page) Sleeper(pollTimeout, timeout time.Duration) *Page {
|
||||
p.mutex.RLock()
|
||||
page := *p
|
||||
p.mutex.RUnlock()
|
||||
page.page = page.Page().Sleeper(func() utils.Sleeper {
|
||||
return createBackOffSleeper(pollTimeout, timeout)
|
||||
})
|
||||
@@ -238,7 +257,9 @@ func (p *Page) Sleeper(pollTimeout, timeout time.Duration) *Page {
|
||||
}
|
||||
|
||||
func (p *Page) Timeout(timeout time.Duration) *Page {
|
||||
p.mutex.RLock()
|
||||
page := *p
|
||||
p.mutex.RUnlock()
|
||||
page.page = page.Page().Timeout(timeout)
|
||||
return &page
|
||||
}
|
||||
@@ -732,6 +753,18 @@ func (p *Page) SelectInputElement(act *Action, out ActionData) error {
|
||||
|
||||
// WaitPageLifecycleEvent waits for specified page lifecycle event name
|
||||
func (p *Page) WaitPageLifecycleEvent(act *Action, out ActionData, event proto.PageLifecycleEventName) error {
|
||||
if event == proto.PageLifecycleEventNameLoad {
|
||||
dur, err := getTimeout(p, act)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.page.Timeout(dur).WaitLoad(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.updateLastNavigatedURL()
|
||||
return nil
|
||||
}
|
||||
|
||||
fn, err := getNavigationFunc(p, act, event)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testheadless"
|
||||
@@ -153,6 +155,26 @@ func TestLocatorInteractshTracking(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSleeperConcurrentHistoryUpdate(t *testing.T) {
|
||||
page := &Page{page: &rod.Page{}, mutex: &sync.RWMutex{}}
|
||||
start := make(chan struct{})
|
||||
var done sync.WaitGroup
|
||||
done.Add(1)
|
||||
go func() {
|
||||
defer done.Done()
|
||||
<-start
|
||||
for range 10000 {
|
||||
page.addToHistory(HistoryData{})
|
||||
}
|
||||
}()
|
||||
|
||||
close(start)
|
||||
for range 10000 {
|
||||
page.Sleeper(time.Millisecond, time.Second)
|
||||
}
|
||||
done.Wait()
|
||||
}
|
||||
|
||||
func TestActionNavigate(t *testing.T) {
|
||||
response := `
|
||||
<html>
|
||||
@@ -642,6 +664,47 @@ func TestActionWaitLoad(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionWaitLoadAfterPageAlreadyLoaded(t *testing.T) {
|
||||
actions := []*Action{{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}}}
|
||||
testHeadlessSimpleResponse(t, "<html><body>ready</body></html>", actions, 10*time.Second, func(page *Page, err error, _ ActionData) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, page.Page().WaitLoad())
|
||||
|
||||
wait := &Action{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}, Data: map[string]string{"timeout": "2s"}}
|
||||
started := time.Now()
|
||||
require.NoError(t, page.WaitPageLifecycleEvent(wait, nil, proto.PageLifecycleEventNameLoad))
|
||||
require.Less(t, time.Since(started), 1500*time.Millisecond, "a completed load must not consume the full wait timeout")
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionWaitLoadWaitsForPendingResource(t *testing.T) {
|
||||
resourceDone := make(chan struct{})
|
||||
actions := []*Action{
|
||||
{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
|
||||
{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
|
||||
}
|
||||
testHeadless(t, actions, 10*time.Second, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/slow.js" {
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_, _ = fmt.Fprint(w, "window.resourceLoaded = true")
|
||||
close(resourceDone)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, `<html><script src="/slow.js"></script></html>`)
|
||||
}, func(page *Page, err error, _ ActionData) {
|
||||
require.NoError(t, err)
|
||||
select {
|
||||
case <-resourceDone:
|
||||
default:
|
||||
t.Fatal("waitload returned before the pending resource completed")
|
||||
}
|
||||
loaded, evalErr := page.Page().Eval(`() => window.resourceLoaded`)
|
||||
require.NoError(t, evalErr)
|
||||
require.True(t, loaded.Value.Bool())
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionGetResource(t *testing.T) {
|
||||
response := `
|
||||
<html>
|
||||
@@ -839,6 +902,25 @@ func TestActionWaitEventDuration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionNavigateDurationIncludesResponseHeaderWait(t *testing.T) {
|
||||
actions := []*Action{
|
||||
{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
|
||||
{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
|
||||
}
|
||||
|
||||
handler := func(w http.ResponseWriter, _ *http.Request) {
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
_, _ = fmt.Fprintln(w, `<html><body>loaded</body></html>`)
|
||||
}
|
||||
|
||||
testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, _ ActionData) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, page.ActionDurations, 2)
|
||||
require.GreaterOrEqual(t, page.ActionDurations[0], 300*time.Millisecond)
|
||||
require.Less(t, page.ActionDurations[1], time.Second)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionWaitEventSeesEventFiredBeforeWaiting(t *testing.T) {
|
||||
actions := []*Action{
|
||||
{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
|
||||
@@ -1071,10 +1153,21 @@ func TestHeadlessRunHonorsParentCancellation(t *testing.T) {
|
||||
}}
|
||||
|
||||
startedAt := time.Now()
|
||||
_, page, err := instance.Run(input, actions, nil, &Options{Timeout: 5 * time.Second, Options: opts})
|
||||
var timedAction ActionType
|
||||
var timedDuration time.Duration
|
||||
_, page, err := instance.Run(input, actions, nil, &Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Options: opts,
|
||||
OnActionTiming: func(action ActionType, duration time.Duration) {
|
||||
timedAction = action
|
||||
timedDuration = duration
|
||||
},
|
||||
})
|
||||
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
require.Nil(t, page)
|
||||
require.Equal(t, ActionNavigate, timedAction)
|
||||
require.Positive(t, timedDuration)
|
||||
require.Less(t, time.Since(startedAt), 2*time.Second)
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,10 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata,
|
||||
if len(request.Fuzzing) > 0 {
|
||||
return request.executeFuzzingRule(input, vars, previous, wrappedCallback)
|
||||
}
|
||||
timing := newHeadlessTimingTrace(request.options.TemplateID, input.MetaInput.Input)
|
||||
if timing != nil {
|
||||
defer timing.emit(request.options.Logger)
|
||||
}
|
||||
if request.generator != nil {
|
||||
iterator := request.generator.NewIterator()
|
||||
for {
|
||||
@@ -103,13 +107,13 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata,
|
||||
return errors.Wrap(err, "could not evaluate payload helper expressions")
|
||||
}
|
||||
|
||||
if err := request.executeRequestWithPayloads(input, renderedValue.Values, previous, renderedValue.InteractURLs, wrappedCallback); err != nil {
|
||||
if err := request.executeRequestWithPayloads(input, renderedValue.Values, previous, renderedValue.InteractURLs, wrappedCallback, timing); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value := maps.Clone(vars)
|
||||
if err := request.executeRequestWithPayloads(input, value, previous, interactshURLs, wrappedCallback); err != nil {
|
||||
if err := request.executeRequestWithPayloads(input, value, previous, interactshURLs, wrappedCallback, timing); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -132,9 +136,15 @@ func extractBaseURLFromActions(steps []*engine.Action) (string, error) {
|
||||
return "", errors.New("no navigation action found")
|
||||
}
|
||||
|
||||
func (request *Request) executeRequestWithPayloads(input *contextargs.Context, payloads map[string]interface{}, previous output.InternalEvent, interactshURLs []string, callback protocols.OutputEventCallback) error {
|
||||
func (request *Request) executeRequestWithPayloads(input *contextargs.Context, payloads map[string]interface{}, previous output.InternalEvent, interactshURLs []string, callback protocols.OutputEventCallback, timing *headlessTimingTrace) error {
|
||||
if timing != nil {
|
||||
timing.attempts++
|
||||
}
|
||||
pageTimeout := time.Duration(request.options.Options.PageTimeout) * time.Second
|
||||
if _, err := url.Parse(input.MetaInput.Input); err != nil {
|
||||
if timing != nil {
|
||||
timing.errors++
|
||||
}
|
||||
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
|
||||
request.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(err, errCouldNotGetHtmlElement)
|
||||
@@ -144,35 +154,82 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
|
||||
DisableCookie: request.DisableCookie,
|
||||
Options: request.options.Options,
|
||||
}
|
||||
var actionTimings []headlessActionTiming
|
||||
if timing != nil {
|
||||
options.OnActionTiming = func(action engine.ActionType, duration time.Duration) {
|
||||
actionTimings = append(actionTimings, headlessActionTiming{action: action, duration: duration})
|
||||
}
|
||||
}
|
||||
|
||||
if !options.DisableCookie && input.CookieJar == nil {
|
||||
if timing != nil {
|
||||
timing.errors++
|
||||
}
|
||||
return errors.New("cookie reuse enabled but cookie-jar is nil")
|
||||
}
|
||||
|
||||
// limited as one request per run of the steps, the unit progress counts
|
||||
if err := request.options.RateLimitTakeContext(input.Context(), input.MetaInput.Input); err != nil {
|
||||
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
|
||||
var rateWaitStarted time.Time
|
||||
if timing != nil {
|
||||
rateWaitStarted = time.Now()
|
||||
}
|
||||
rateWaitErr := request.options.RateLimitTakeContext(input.Context(), input.MetaInput.Input)
|
||||
if timing != nil {
|
||||
timing.rateWait += time.Since(rateWaitStarted)
|
||||
}
|
||||
if rateWaitErr != nil {
|
||||
if timing != nil {
|
||||
timing.errors++
|
||||
}
|
||||
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), rateWaitErr)
|
||||
request.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(err, "could not take rate limit")
|
||||
return errors.Wrap(rateWaitErr, "could not take rate limit")
|
||||
}
|
||||
|
||||
var createStarted time.Time
|
||||
if timing != nil {
|
||||
createStarted = time.Now()
|
||||
}
|
||||
instance, err := request.options.Browser.NewInstanceWithContext(input.Context(), pageTimeout)
|
||||
if timing != nil {
|
||||
timing.contextCreate += time.Since(createStarted)
|
||||
}
|
||||
if err != nil {
|
||||
if timing != nil {
|
||||
timing.errors++
|
||||
}
|
||||
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
|
||||
request.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(err, errCouldNotGetHtmlElement)
|
||||
}
|
||||
var page *engine.Page
|
||||
defer func() {
|
||||
var cleanupStarted time.Time
|
||||
if timing != nil {
|
||||
cleanupStarted = time.Now()
|
||||
}
|
||||
if closeErr := instance.CloseWithPage(page); closeErr != nil {
|
||||
gologger.Warning().Msgf("Could not close headless browser instance: %s", closeErr)
|
||||
}
|
||||
if timing != nil {
|
||||
timing.cleanup += time.Since(cleanupStarted)
|
||||
}
|
||||
}()
|
||||
instance.SetInteractsh(request.options.Interactsh)
|
||||
|
||||
timeStart := time.Now()
|
||||
out, createdPage, err := instance.Run(input, request.Steps, payloads, options)
|
||||
runDuration := time.Since(timeStart)
|
||||
if timing != nil {
|
||||
if err != nil {
|
||||
timing.errors++
|
||||
timing.recordFailedRun(actionTimings, runDuration)
|
||||
} else if createdPage != nil {
|
||||
timing.recordRun(request.Steps, createdPage.ActionDurations, runDuration)
|
||||
} else {
|
||||
timing.runOther += runDuration
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
|
||||
request.options.Progress.IncrementFailedRequestsBy(1)
|
||||
@@ -294,7 +351,7 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, payloads
|
||||
newInput := input.Clone()
|
||||
newInput.MetaInput.Input = gr.Request.String()
|
||||
|
||||
if err := request.executeRequestWithPayloads(newInput, gr.DynamicValues, previous, gr.InteractURLs, callback); err != nil {
|
||||
if err := request.executeRequestWithPayloads(newInput, gr.DynamicValues, previous, gr.InteractURLs, callback, nil); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package headless
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
)
|
||||
|
||||
const (
|
||||
headlessTimingTemplatesEnv = "NUCLEI_HEADLESS_TIMING_TEMPLATES"
|
||||
headlessTimingSampleRate = 50
|
||||
headlessTimingSampleMin = 30 * time.Second
|
||||
headlessTimingOutlierMin = 5 * time.Minute
|
||||
)
|
||||
|
||||
type headlessTimingTrace struct {
|
||||
templateID string
|
||||
targetFingerprint string
|
||||
sampled bool
|
||||
started time.Time
|
||||
attempts int
|
||||
errors int
|
||||
rateWait time.Duration
|
||||
contextCreate time.Duration
|
||||
navigate time.Duration
|
||||
waitLoad time.Duration
|
||||
waitDialog time.Duration
|
||||
otherActions time.Duration
|
||||
runOther time.Duration
|
||||
cleanup time.Duration
|
||||
}
|
||||
|
||||
type headlessActionTiming struct {
|
||||
action engine.ActionType
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func newHeadlessTimingTrace(templateID, target string) *headlessTimingTrace {
|
||||
selectedTemplates := os.Getenv(headlessTimingTemplatesEnv)
|
||||
if selectedTemplates == "" {
|
||||
return nil
|
||||
}
|
||||
selected := false
|
||||
for _, candidate := range strings.Split(selectedTemplates, ",") {
|
||||
if strings.TrimSpace(candidate) == templateID {
|
||||
selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !selected {
|
||||
return nil
|
||||
}
|
||||
fingerprint := sha256.Sum256([]byte(target))
|
||||
return &headlessTimingTrace{
|
||||
templateID: templateID,
|
||||
targetFingerprint: fmt.Sprintf("%x", fingerprint[:8]),
|
||||
sampled: binary.BigEndian.Uint64(fingerprint[:8])%headlessTimingSampleRate == 0,
|
||||
started: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *headlessTimingTrace) recordRun(steps []*engine.Action, durations []time.Duration, elapsed time.Duration) {
|
||||
actions := time.Duration(0)
|
||||
if len(steps) != len(durations) {
|
||||
for _, duration := range durations {
|
||||
actions += duration
|
||||
}
|
||||
t.otherActions += actions
|
||||
if elapsed > actions {
|
||||
t.runOther += elapsed - actions
|
||||
}
|
||||
return
|
||||
}
|
||||
for index, duration := range durations {
|
||||
actions += duration
|
||||
if steps[index] == nil {
|
||||
t.otherActions += duration
|
||||
continue
|
||||
}
|
||||
switch steps[index].ActionType.ActionType {
|
||||
case engine.ActionNavigate:
|
||||
t.navigate += duration
|
||||
case engine.ActionWaitLoad:
|
||||
t.waitLoad += duration
|
||||
case engine.ActionWaitDialog:
|
||||
t.waitDialog += duration
|
||||
default:
|
||||
t.otherActions += duration
|
||||
}
|
||||
}
|
||||
if elapsed > actions {
|
||||
t.runOther += elapsed - actions
|
||||
}
|
||||
}
|
||||
|
||||
func (t *headlessTimingTrace) recordFailedRun(actions []headlessActionTiming, elapsed time.Duration) {
|
||||
measured := time.Duration(0)
|
||||
for _, action := range actions {
|
||||
measured += action.duration
|
||||
switch action.action {
|
||||
case engine.ActionNavigate:
|
||||
t.navigate += action.duration
|
||||
case engine.ActionWaitLoad:
|
||||
t.waitLoad += action.duration
|
||||
case engine.ActionWaitDialog:
|
||||
t.waitDialog += action.duration
|
||||
default:
|
||||
t.otherActions += action.duration
|
||||
}
|
||||
}
|
||||
t.runOther += max(elapsed-measured, 0)
|
||||
}
|
||||
|
||||
func (t *headlessTimingTrace) summary(elapsed time.Duration) (string, bool) {
|
||||
if elapsed < headlessTimingOutlierMin && (!t.sampled || elapsed < headlessTimingSampleMin) {
|
||||
return "", false
|
||||
}
|
||||
measured := t.rateWait + t.contextCreate + t.navigate + t.waitLoad + t.waitDialog + t.otherActions + t.runOther + t.cleanup
|
||||
unattributed := max(elapsed-measured, 0)
|
||||
return fmt.Sprintf(
|
||||
"slow headless timing template_id=%q target_fingerprint=%s attempts=%d errors=%d total_ms=%d rate_wait_ms=%d context_create_ms=%d navigate_ms=%d waitload_ms=%d waitdialog_ms=%d other_actions_ms=%d run_other_ms=%d cleanup_ms=%d unattributed_ms=%d",
|
||||
t.templateID, t.targetFingerprint, t.attempts, t.errors, elapsed.Milliseconds(), t.rateWait.Milliseconds(),
|
||||
t.contextCreate.Milliseconds(), t.navigate.Milliseconds(), t.waitLoad.Milliseconds(), t.waitDialog.Milliseconds(),
|
||||
t.otherActions.Milliseconds(), t.runOther.Milliseconds(), t.cleanup.Milliseconds(), unattributed.Milliseconds(),
|
||||
), true
|
||||
}
|
||||
|
||||
func (t *headlessTimingTrace) emit(logger *gologger.Logger) {
|
||||
if message, ok := t.summary(time.Since(t.started)); ok {
|
||||
if logger == nil {
|
||||
logger = gologger.DefaultLogger
|
||||
}
|
||||
logger.Info().Msg(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package headless
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/formatter"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type timingCaptureWriter struct{ buffer *bytes.Buffer }
|
||||
|
||||
func (w timingCaptureWriter) Write(data []byte, _ levels.Level) {
|
||||
w.buffer.Write(data)
|
||||
}
|
||||
|
||||
func TestHeadlessTimingTraceUsesExecutionLogger(t *testing.T) {
|
||||
previous := gologger.DefaultLogger
|
||||
t.Cleanup(func() { gologger.DefaultLogger = previous })
|
||||
var output bytes.Buffer
|
||||
silentLogger := &gologger.Logger{}
|
||||
silentLogger.SetFormatter(formatter.NewCLI(false))
|
||||
silentLogger.SetWriter(timingCaptureWriter{buffer: &output})
|
||||
silentLogger.SetMaxLevel(levels.LevelSilent)
|
||||
gologger.DefaultLogger = silentLogger
|
||||
logger := &gologger.Logger{}
|
||||
logger.SetFormatter(formatter.NewCLI(false))
|
||||
logger.SetWriter(timingCaptureWriter{buffer: &output})
|
||||
logger.SetMaxLevel(levels.LevelInfo)
|
||||
trace := &headlessTimingTrace{templateID: "headless-open-redirect", sampled: true, started: time.Now().Add(-31 * time.Second)}
|
||||
|
||||
trace.emit(silentLogger)
|
||||
trace.emit(nil)
|
||||
require.Empty(t, output.String())
|
||||
|
||||
trace.emit(logger)
|
||||
require.Contains(t, output.String(), "slow headless timing template_id=\"headless-open-redirect\"")
|
||||
}
|
||||
|
||||
func TestHeadlessTimingTraceIsTemplateOptIn(t *testing.T) {
|
||||
t.Setenv(headlessTimingTemplatesEnv, "")
|
||||
require.Nil(t, newHeadlessTimingTrace("headless-open-redirect", "https://example.com"))
|
||||
|
||||
t.Setenv(headlessTimingTemplatesEnv, "CVE-2025-8191, headless-open-redirect")
|
||||
require.Nil(t, newHeadlessTimingTrace("other-template", "https://example.com"))
|
||||
|
||||
trace := newHeadlessTimingTrace("headless-open-redirect", "https://example.com")
|
||||
require.NotNil(t, trace)
|
||||
require.Len(t, trace.targetFingerprint, 16)
|
||||
require.NotContains(t, trace.targetFingerprint, "example.com")
|
||||
}
|
||||
|
||||
func TestHeadlessTimingTraceAccountsForActionsAndOverhead(t *testing.T) {
|
||||
trace := &headlessTimingTrace{
|
||||
templateID: "headless-open-redirect",
|
||||
targetFingerprint: "0123456789abcdef",
|
||||
sampled: true,
|
||||
attempts: 2,
|
||||
errors: 1,
|
||||
rateWait: time.Second,
|
||||
contextCreate: time.Second,
|
||||
cleanup: 250 * time.Millisecond,
|
||||
}
|
||||
steps := []*engine.Action{
|
||||
{ActionType: engine.ActionTypeHolder{ActionType: engine.ActionNavigate}},
|
||||
{ActionType: engine.ActionTypeHolder{ActionType: engine.ActionWaitLoad}},
|
||||
{ActionType: engine.ActionTypeHolder{ActionType: engine.ActionWaitDialog}},
|
||||
}
|
||||
trace.recordRun(steps, []time.Duration{2 * time.Second, 500 * time.Millisecond, 3 * time.Second}, 6*time.Second)
|
||||
|
||||
message, ok := trace.summary(30 * time.Second)
|
||||
require.True(t, ok)
|
||||
for _, field := range []string{
|
||||
"attempts=2", "errors=1", "total_ms=30000", "rate_wait_ms=1000",
|
||||
"context_create_ms=1000", "navigate_ms=2000", "waitload_ms=500",
|
||||
"waitdialog_ms=3000", "run_other_ms=500", "cleanup_ms=250", "unattributed_ms=21750",
|
||||
} {
|
||||
require.True(t, strings.Contains(message, field), "missing %s in %s", field, message)
|
||||
}
|
||||
require.NotContains(t, message, "https://")
|
||||
}
|
||||
|
||||
func TestHeadlessTimingTraceBoundsOutput(t *testing.T) {
|
||||
trace := &headlessTimingTrace{sampled: false}
|
||||
_, ok := trace.summary(4 * time.Minute)
|
||||
require.False(t, ok)
|
||||
_, ok = trace.summary(5 * time.Minute)
|
||||
require.True(t, ok)
|
||||
|
||||
trace.sampled = true
|
||||
_, ok = trace.summary(29 * time.Second)
|
||||
require.False(t, ok)
|
||||
_, ok = trace.summary(30 * time.Second)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
func TestHeadlessTimingTraceDoesNotMisattributePartialActionDurations(t *testing.T) {
|
||||
trace := &headlessTimingTrace{sampled: true}
|
||||
steps := []*engine.Action{
|
||||
{ActionType: engine.ActionTypeHolder{ActionType: engine.ActionNavigate}},
|
||||
{ActionType: engine.ActionTypeHolder{ActionType: engine.ActionWaitLoad}},
|
||||
}
|
||||
trace.recordRun(steps, []time.Duration{2 * time.Second}, 3*time.Second)
|
||||
|
||||
require.Zero(t, trace.navigate)
|
||||
require.Zero(t, trace.waitLoad)
|
||||
require.Equal(t, 2*time.Second, trace.otherActions)
|
||||
require.Equal(t, time.Second, trace.runOther)
|
||||
}
|
||||
|
||||
func TestHeadlessTimingTraceAttributesFailedActions(t *testing.T) {
|
||||
trace := &headlessTimingTrace{sampled: true}
|
||||
trace.recordFailedRun([]headlessActionTiming{
|
||||
{action: engine.ActionNavigate, duration: 2 * time.Second},
|
||||
{action: engine.ActionWaitLoad, duration: 500 * time.Millisecond},
|
||||
}, 3*time.Second)
|
||||
|
||||
require.Equal(t, 2*time.Second, trace.navigate)
|
||||
require.Equal(t, 500*time.Millisecond, trace.waitLoad)
|
||||
require.Zero(t, trace.otherActions)
|
||||
require.Equal(t, 500*time.Millisecond, trace.runOther)
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
|
||||
@@ -547,12 +546,8 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
|
||||
}
|
||||
}
|
||||
if len(request.Payloads) > 0 {
|
||||
// Due to a known issue (https://github.com/projectdiscovery/nuclei/issues/5015),
|
||||
// dynamic extractors cannot be used with payloads. To address this,
|
||||
// execution is handled by the standard engine without concurrency,
|
||||
// achieved by setting the thread count to 0.
|
||||
|
||||
// this limitation will be removed once we have a better way to handle dynamic extractors with payloads
|
||||
// Multi-request matchers and dynamic extractors depend on ordered responses
|
||||
// across a payload iteration, so they use the standard engine.
|
||||
hasMultipleRequests := false
|
||||
if len(request.Raw)+len(request.Path) > 1 {
|
||||
hasMultipleRequests = true
|
||||
@@ -565,14 +560,10 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasNamedInternalExtractor && hasMultipleRequests {
|
||||
if hasMultipleRequests && (hasNamedInternalExtractor || request.NeedsRequestCondition()) {
|
||||
stats.Increment(SetThreadToCountZero)
|
||||
request.Threads = 0
|
||||
} else {
|
||||
// specifically for http requests high concurrency and threads will lead to memory exhaustion, hence reduce the maximum parallelism
|
||||
if protocolstate.IsLowOnMemory() {
|
||||
request.Threads = protocolstate.GuardThreadsOrDefault(request.Threads)
|
||||
}
|
||||
request.Threads = options.GetThreadsForNPayloadRequests(request.Requests(), request.Threads)
|
||||
}
|
||||
}
|
||||
@@ -622,7 +613,7 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
stats.NewEntry(SetThreadToCountZero, "Setting thread count to 0 for %d templates, dynamic extractors are not supported with payloads yet")
|
||||
stats.NewEntry(SetThreadToCountZero, "Setting thread count to 0 for %d templates with payloads and dependent requests")
|
||||
}
|
||||
|
||||
// UpdateOptions replaces this request's options with a new copy
|
||||
|
||||
@@ -58,7 +58,7 @@ func NewBlockingSPMHandler[T comparable](ctx context.Context, size int, maxResul
|
||||
maxResults: maxResults,
|
||||
}
|
||||
s.internalWg.Add(1)
|
||||
go s.run(ctx)
|
||||
go s.run(ctx1)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func NewNonBlockingSPMHandler[T comparable](ctx context.Context, maxResults int,
|
||||
maxResults: maxResults,
|
||||
}
|
||||
s.internalWg.Add(1)
|
||||
go s.run(ctx)
|
||||
go s.run(ctx1)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -132,18 +132,20 @@ func (h *StopAtFirstMatchHandler[T]) run(ctx context.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if h.onResult != nil {
|
||||
h.onResult(val)
|
||||
}
|
||||
if len(h.results) >= h.maxResults {
|
||||
// skip or do not store the result
|
||||
continue
|
||||
}
|
||||
h.results[val] = struct{}{}
|
||||
h.recordResult(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *StopAtFirstMatchHandler[T]) recordResult(val T) {
|
||||
if h.onResult != nil {
|
||||
h.onResult(val)
|
||||
}
|
||||
if len(h.results) < h.maxResults {
|
||||
h.results[val] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Context is canceled when the handler stops.
|
||||
func (h *StopAtFirstMatchHandler[T]) Context() context.Context {
|
||||
return h.ctx
|
||||
@@ -210,6 +212,9 @@ func (h *StopAtFirstMatchHandler[T]) Wait() {
|
||||
// after waiting it closes the error channel
|
||||
close(h.ResultChan)
|
||||
h.internalWg.Wait()
|
||||
for val := range h.ResultChan {
|
||||
h.recordResult(val)
|
||||
}
|
||||
}
|
||||
|
||||
// CombinedResults returns the combined results
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package httputils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCancelStopsResultReceiver(t *testing.T) {
|
||||
constructors := map[string]func(context.Context) *StopAtFirstMatchHandler[int]{
|
||||
"blocking": func(ctx context.Context) *StopAtFirstMatchHandler[int] {
|
||||
return NewBlockingSPMHandler[int](ctx, 1, 1, true)
|
||||
},
|
||||
"nonblocking": func(ctx context.Context) *StopAtFirstMatchHandler[int] {
|
||||
return NewNonBlockingSPMHandler[int](ctx, 1, true)
|
||||
},
|
||||
}
|
||||
|
||||
for name, construct := range constructors {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
parent, cancelParent := context.WithCancel(context.Background())
|
||||
defer cancelParent()
|
||||
handler := construct(parent)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
handler.internalWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
handler.Cancel()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("result receiver remained alive after cancellation")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelPreservesQueuedResults(t *testing.T) {
|
||||
constructors := map[string]func() *StopAtFirstMatchHandler[int]{
|
||||
"blocking": func() *StopAtFirstMatchHandler[int] {
|
||||
return NewBlockingSPMHandler[int](context.Background(), 1, 2, false)
|
||||
},
|
||||
"nonblocking": func() *StopAtFirstMatchHandler[int] {
|
||||
return NewNonBlockingSPMHandler[int](context.Background(), 2, false)
|
||||
},
|
||||
}
|
||||
|
||||
for name, construct := range constructors {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for range 25 {
|
||||
handler := construct()
|
||||
entered := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
handler.SetOnResultCallback(func(value int) {
|
||||
if value == 1 {
|
||||
close(entered)
|
||||
<-release
|
||||
}
|
||||
})
|
||||
handler.ResultChan <- 1
|
||||
<-entered
|
||||
handler.ResultChan <- 2
|
||||
handler.Cancel()
|
||||
close(release)
|
||||
handler.Wait()
|
||||
|
||||
found := make(map[int]bool)
|
||||
for _, result := range handler.CombinedResults() {
|
||||
found[result] = true
|
||||
}
|
||||
if !found[1] || !found[2] {
|
||||
t.Fatalf("queued results lost after cancellation: %v", found)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -122,13 +122,26 @@ func (request *Request) waitHostBackoff(ctx context.Context, host string) error
|
||||
return request.options.HostBackoff.Wait(ctx, hostratelimit.NormalizeHostPort(host))
|
||||
}
|
||||
|
||||
type httpRateLimitWaitObserver interface {
|
||||
ObserveHTTPRateLimitWait(time.Duration)
|
||||
}
|
||||
|
||||
type httpRequestTimingObserver interface {
|
||||
ObserveHTTPRequest(time.Duration, int, error)
|
||||
}
|
||||
|
||||
// rateLimitTake first honours any backoff the host earned by signalling it is
|
||||
// overloaded, so a blocking target is paced regardless of which limiter is in play.
|
||||
func (request *Request) rateLimitTake(ctx context.Context, hostname string) error {
|
||||
if err := request.waitHostBackoff(ctx, hostname); err != nil {
|
||||
return err
|
||||
}
|
||||
return request.options.RateLimitTakeContext(ctx, hostname)
|
||||
started := time.Now()
|
||||
err := request.options.RateLimitTakeContext(ctx, hostname)
|
||||
if observer, ok := request.options.Progress.(httpRateLimitWaitObserver); ok {
|
||||
observer.ObserveHTTPRateLimitWait(time.Since(started))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// stopWithHandler ends when parent ends or the handler stops, so a backoff
|
||||
@@ -419,8 +432,12 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
|
||||
}
|
||||
|
||||
// resize check point - nop if there are no changes
|
||||
if shouldFollowGlobal && spmHandler.Size() != request.options.Options.PayloadConcurrency {
|
||||
if err := spmHandler.Resize(input.Context(), request.options.Options.PayloadConcurrency); err != nil {
|
||||
desiredWorkers := request.options.Options.PayloadConcurrency
|
||||
if protocolstate.IsLowOnMemory() {
|
||||
desiredWorkers = protocolstate.GuardThreadsOrDefault(desiredWorkers)
|
||||
}
|
||||
if shouldFollowGlobal && spmHandler.Size() != desiredWorkers {
|
||||
if err := spmHandler.Resize(input.Context(), desiredWorkers); err != nil {
|
||||
close(tasks)
|
||||
workersWg.Wait()
|
||||
return err
|
||||
@@ -679,16 +696,18 @@ executionLoop:
|
||||
// a copy and using it to check for host errors etc
|
||||
// but this should be replaced once templateCtx is refactored properly
|
||||
updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedHttpRequest.URL())
|
||||
if generatedHttpRequest.customCancelFunction != nil {
|
||||
defer generatedHttpRequest.customCancelFunction()
|
||||
}
|
||||
if request.isUnresponsiveAddress(updatedInput) {
|
||||
return false, true, nil, nil
|
||||
}
|
||||
|
||||
// Full URL, so https and an explicit port stay on the same key the observation uses.
|
||||
if err := request.rateLimitTake(ctx, hostBackoffKey(generatedHttpRequest, input)); err != nil {
|
||||
return false, true, nil, err
|
||||
}
|
||||
|
||||
if generatedHttpRequest.customCancelFunction != nil {
|
||||
defer generatedHttpRequest.customCancelFunction()
|
||||
}
|
||||
|
||||
hasInteractMarkers := interactsh.HasMarkers(data) || len(generatedHttpRequest.interactshURLs) > 0
|
||||
if input.MetaInput.Input == "" {
|
||||
input.MetaInput.Input = generatedHttpRequest.URL()
|
||||
@@ -1247,6 +1266,13 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
|
||||
// Record before dump or the dialer lookup can return and drop the outcome.
|
||||
if sent {
|
||||
request.observeHostBackoff(backoffKey, resp, err)
|
||||
if observer, ok := request.options.Progress.(httpRequestTimingObserver); ok {
|
||||
statusCode := 0
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
}
|
||||
observer.ObserveHTTPRequest(time.Since(timeStart), statusCode, err)
|
||||
}
|
||||
}
|
||||
// use request url as matched url if empty
|
||||
if formedURL == "" {
|
||||
|
||||
@@ -21,10 +21,15 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators/extractors"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/projectdiscovery/utils/memguardian"
|
||||
"io"
|
||||
)
|
||||
|
||||
@@ -1296,6 +1301,291 @@ func TestExecuteParallelHTTP_StopAtFirstMatchCancelsQueuedRateLimitWaiters(t *te
|
||||
require.Equal(t, int32(1), requests.Load())
|
||||
}
|
||||
|
||||
func TestPayloadConcurrencyIsGuardedOnceUnderMemoryPressure(t *testing.T) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.ExecutionId = t.Name()
|
||||
options.RestrictLocalNetworkAccess = false
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
require.NotNil(t, memguardian.DefaultMemGuardian)
|
||||
previousWarning := memguardian.DefaultMemGuardian.Warning.Swap(true)
|
||||
t.Cleanup(func() { memguardian.DefaultMemGuardian.Warning.Store(previousWarning) })
|
||||
|
||||
var active atomic.Int32
|
||||
var maximum atomic.Int32
|
||||
var total atomic.Int32
|
||||
var matched atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
current := active.Add(1)
|
||||
for previous := maximum.Load(); current > previous && !maximum.CompareAndSwap(previous, current); previous = maximum.Load() {
|
||||
}
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
active.Add(-1)
|
||||
total.Add(1)
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
request := &Request{
|
||||
ID: "memory-guard-payload-concurrency",
|
||||
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
|
||||
Path: []string{"{{BaseURL}}/test?payload={{v}}"},
|
||||
Threads: 20,
|
||||
Payloads: map[string]interface{}{
|
||||
"v": []string{"1", "2", "3", "4", "5", "6", "7", "8"},
|
||||
},
|
||||
Operators: operators.Operators{Matchers: []*matchers.Matcher{{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},
|
||||
DSL: []string{"status_code == 200"},
|
||||
}}},
|
||||
}
|
||||
executerOptions := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: request.ID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
executerOptions.RateLimiter = nil
|
||||
require.NoError(t, request.Compile(executerOptions))
|
||||
compiledThreads := request.Threads
|
||||
require.NoError(t, request.ExecuteWithResults(
|
||||
contextargs.NewWithInput(t.Context(), server.URL),
|
||||
make(output.InternalEvent),
|
||||
make(output.InternalEvent),
|
||||
func(event *output.InternalWrappedEvent) {
|
||||
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
|
||||
matched.Add(1)
|
||||
}
|
||||
},
|
||||
))
|
||||
require.Equal(t, int32(8), total.Load())
|
||||
require.Equal(t, int32(8), matched.Load())
|
||||
if compiledThreads != 20 || maximum.Load() <= 1 {
|
||||
t.Fatalf("memory guard applied more than once: compiled threads=%d, maximum concurrent requests=%d", compiledThreads, maximum.Load())
|
||||
}
|
||||
|
||||
memguardian.DefaultMemGuardian.Warning.Store(false)
|
||||
maximum.Store(0)
|
||||
total.Store(0)
|
||||
matched.Store(0)
|
||||
require.NoError(t, request.ExecuteWithResults(
|
||||
contextargs.NewWithInput(t.Context(), server.URL),
|
||||
make(output.InternalEvent),
|
||||
make(output.InternalEvent),
|
||||
func(event *output.InternalWrappedEvent) {
|
||||
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
|
||||
matched.Add(1)
|
||||
}
|
||||
},
|
||||
))
|
||||
require.Equal(t, int32(8), total.Load())
|
||||
require.Equal(t, int32(8), matched.Load())
|
||||
require.Greater(t, maximum.Load(), int32(4))
|
||||
}
|
||||
|
||||
func TestDefaultPayloadConcurrencyRespectsMemoryPressure(t *testing.T) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.ExecutionId = t.Name()
|
||||
options.RestrictLocalNetworkAccess = false
|
||||
options.PayloadConcurrency = 25
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
require.NotNil(t, memguardian.DefaultMemGuardian)
|
||||
previousWarning := memguardian.DefaultMemGuardian.Warning.Swap(true)
|
||||
t.Cleanup(func() { memguardian.DefaultMemGuardian.Warning.Store(previousWarning) })
|
||||
|
||||
var active atomic.Int32
|
||||
var maximum atomic.Int32
|
||||
var total atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
current := active.Add(1)
|
||||
for previous := maximum.Load(); current > previous && !maximum.CompareAndSwap(previous, current); previous = maximum.Load() {
|
||||
}
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
active.Add(-1)
|
||||
total.Add(1)
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
request := &Request{
|
||||
ID: "default-payload-concurrency-under-memory-pressure",
|
||||
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
|
||||
Path: []string{"{{BaseURL}}/test?payload={{v}}"},
|
||||
Threads: options.PayloadConcurrency,
|
||||
Payloads: map[string]interface{}{
|
||||
"v": []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"},
|
||||
},
|
||||
Operators: operators.Operators{Matchers: []*matchers.Matcher{{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},
|
||||
DSL: []string{"status_code == 200"},
|
||||
}}},
|
||||
}
|
||||
executerOptions := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: request.ID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
executerOptions.RateLimiter = nil
|
||||
require.NoError(t, request.Compile(executerOptions))
|
||||
require.NoError(t, request.ExecuteWithResults(
|
||||
contextargs.NewWithInput(t.Context(), server.URL),
|
||||
make(output.InternalEvent),
|
||||
make(output.InternalEvent),
|
||||
func(*output.InternalWrappedEvent) {},
|
||||
))
|
||||
require.Equal(t, int32(16), total.Load())
|
||||
require.LessOrEqual(t, maximum.Load(), int32(protocolstate.GuardThreadsOrDefault(options.PayloadConcurrency)))
|
||||
require.Greater(t, maximum.Load(), int32(1))
|
||||
}
|
||||
|
||||
type timingProgressObserver struct {
|
||||
progress.Progress
|
||||
mu sync.Mutex
|
||||
requestDuration time.Duration
|
||||
requestStatus int
|
||||
requestError error
|
||||
requestCount int
|
||||
rateWaitCount int
|
||||
rateWait time.Duration
|
||||
}
|
||||
|
||||
func (o *timingProgressObserver) ObserveHTTPRateLimitWait(duration time.Duration) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
o.rateWaitCount++
|
||||
o.rateWait = duration
|
||||
}
|
||||
|
||||
func (o *timingProgressObserver) ObserveHTTPRequest(duration time.Duration, status int, err error) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
o.requestDuration = duration
|
||||
o.requestStatus = status
|
||||
o.requestError = err
|
||||
o.requestCount++
|
||||
}
|
||||
|
||||
func TestHTTPExecutionObservesRequestTiming(t *testing.T) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.ExecutionId = t.Name()
|
||||
options.RestrictLocalNetworkAccess = false
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
writer.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
request := &Request{
|
||||
ID: "http-timing-observer",
|
||||
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
|
||||
Path: []string{"{{BaseURL}}/"},
|
||||
Operators: operators.Operators{Matchers: []*matchers.Matcher{{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.StatusMatcher},
|
||||
Status: []int{http.StatusTeapot},
|
||||
}}},
|
||||
}
|
||||
executerOptions := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: request.ID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
observer := &timingProgressObserver{Progress: executerOptions.Progress}
|
||||
executerOptions.Progress = observer
|
||||
require.NoError(t, request.Compile(executerOptions))
|
||||
require.NoError(t, request.ExecuteWithResults(
|
||||
contextargs.NewWithInput(t.Context(), server.URL),
|
||||
make(output.InternalEvent),
|
||||
make(output.InternalEvent),
|
||||
func(*output.InternalWrappedEvent) {},
|
||||
))
|
||||
|
||||
observer.mu.Lock()
|
||||
defer observer.mu.Unlock()
|
||||
require.Equal(t, 1, observer.rateWaitCount)
|
||||
require.Equal(t, 1, observer.requestCount)
|
||||
require.Equal(t, http.StatusTeapot, observer.requestStatus)
|
||||
require.NoError(t, observer.requestError)
|
||||
require.GreaterOrEqual(t, observer.requestDuration, 20*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestHTTPRateLimitWaitExcludesHostBackoff(t *testing.T) {
|
||||
backoff := hostbackoff.New(hostbackoff.Config{Step: 200 * time.Millisecond, Max: 200 * time.Millisecond})
|
||||
observer := &timingProgressObserver{}
|
||||
request := &Request{options: &protocols.ExecutorOptions{HostBackoff: backoff, Progress: observer}}
|
||||
target := "https://example.com/path"
|
||||
request.observeHostBackoff(target, &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{}}, nil)
|
||||
|
||||
started := time.Now()
|
||||
require.NoError(t, request.rateLimitTake(t.Context(), target))
|
||||
elapsed := time.Since(started)
|
||||
|
||||
observer.mu.Lock()
|
||||
defer observer.mu.Unlock()
|
||||
require.Equal(t, 1, observer.rateWaitCount)
|
||||
require.GreaterOrEqual(t, elapsed, 150*time.Millisecond)
|
||||
require.Less(t, observer.rateWait, elapsed/2)
|
||||
}
|
||||
|
||||
func TestHTTPRateLimitWaitNotObservedWhenBackoffCanceled(t *testing.T) {
|
||||
backoff := hostbackoff.New(hostbackoff.Config{Step: time.Second, Max: time.Second})
|
||||
observer := &timingProgressObserver{}
|
||||
request := &Request{options: &protocols.ExecutorOptions{HostBackoff: backoff, Progress: observer}}
|
||||
target := "https://example.com/path"
|
||||
request.observeHostBackoff(target, &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{}}, nil)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
require.ErrorIs(t, request.rateLimitTake(ctx, target), context.Canceled)
|
||||
observer.mu.Lock()
|
||||
defer observer.mu.Unlock()
|
||||
require.Zero(t, observer.rateWaitCount)
|
||||
}
|
||||
|
||||
func TestSerialHTTPUnresponsiveHostSkipsBeforeRateLimit(t *testing.T) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.ExecutionId = t.Name()
|
||||
options.RateLimit = 1
|
||||
options.RateLimitDuration = 2 * time.Second
|
||||
options.RestrictLocalNetworkAccess = false
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
|
||||
var requests atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
requests.Add(1)
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
request := &Request{
|
||||
ID: "serial-skip-before-rate-limit",
|
||||
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
|
||||
Path: []string{"{{BaseURL}}/"},
|
||||
}
|
||||
executerOptions := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: request.ID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
executerOptions.HostErrorsCache = &fakeHostErrorsCache{}
|
||||
observer := &timingProgressObserver{Progress: executerOptions.Progress}
|
||||
executerOptions.Progress = observer
|
||||
executerOptions.RateLimiter = ratelimit.New(context.Background(), 1, 2*time.Second)
|
||||
t.Cleanup(executerOptions.RateLimiter.Stop)
|
||||
require.NoError(t, executerOptions.RateLimiter.TakeContext(t.Context()))
|
||||
require.NoError(t, request.Compile(executerOptions))
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
started := time.Now()
|
||||
err := request.ExecuteWithResults(contextargs.NewWithInput(ctx, server.URL), make(output.InternalEvent), make(output.InternalEvent), func(*output.InternalWrappedEvent) {})
|
||||
require.NoError(t, err)
|
||||
require.Less(t, time.Since(started), 250*time.Millisecond)
|
||||
require.Zero(t, requests.Load())
|
||||
observer.mu.Lock()
|
||||
defer observer.mu.Unlock()
|
||||
require.Zero(t, observer.rateWaitCount)
|
||||
}
|
||||
|
||||
func TestExecuteParallelHTTP_SkipOnUnresponsiveFromCache(t *testing.T) {
|
||||
options := testutils.DefaultOptions
|
||||
testutils.Init(options)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -287,6 +287,10 @@ func newMultiPayloadServer() *httptest.Server {
|
||||
// survive across multiple pitchfork payload iterations (generator restarts).
|
||||
func TestFlowMultiPayloadIteration(t *testing.T) {
|
||||
setup()
|
||||
options := executerOpts.Options
|
||||
previousConcurrency := options.PayloadConcurrency
|
||||
options.PayloadConcurrency = 2
|
||||
t.Cleanup(func() { options.PayloadConcurrency = previousConcurrency })
|
||||
ts := newMultiPayloadServer()
|
||||
defer ts.Close()
|
||||
|
||||
@@ -295,6 +299,7 @@ func TestFlowMultiPayloadIteration(t *testing.T) {
|
||||
|
||||
err = tmpl.Executer.Compile()
|
||||
require.Nil(t, err, "could not compile template")
|
||||
require.Zero(t, tmpl.RequestsHTTP[0].Threads, "request-condition payloads require ordered execution")
|
||||
|
||||
input := contextargs.NewWithInput(context.Background(), ts.URL)
|
||||
ctx := scan.NewScanContext(context.Background(), input)
|
||||
@@ -303,6 +308,24 @@ func TestFlowMultiPayloadIteration(t *testing.T) {
|
||||
require.True(t, gotresults, "expected match with flow + multi payload iteration")
|
||||
}
|
||||
|
||||
func TestIndependentMultiPayloadRequestsRemainParallel(t *testing.T) {
|
||||
setup()
|
||||
options := executerOpts.Options
|
||||
previousConcurrency := options.PayloadConcurrency
|
||||
options.PayloadConcurrency = 2
|
||||
t.Cleanup(func() { options.PayloadConcurrency = previousConcurrency })
|
||||
|
||||
tmpl, err := templates.Parse("testcases/flow-multi-payload-iteration.yaml", nil, executerOpts)
|
||||
require.NoError(t, err)
|
||||
for _, matcher := range tmpl.RequestsHTTP[0].Matchers {
|
||||
matcher.Part = "body"
|
||||
}
|
||||
require.False(t, tmpl.RequestsHTTP[0].NeedsRequestCondition())
|
||||
|
||||
require.NoError(t, tmpl.Executer.Compile())
|
||||
require.Positive(t, tmpl.RequestsHTTP[0].Threads)
|
||||
}
|
||||
|
||||
// TestFlowRequestConditionWithPayloads is the same as above but with
|
||||
// pitchfork payloads and header-based extraction, matching the exact
|
||||
// pattern from issue #5095.
|
||||
|
||||
Reference in new issue
Block a user