package http

import (
	"bytes"
	"context"
	"encoding/hex"
	"fmt"
	"io"
	"maps"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/pkg/errors"
	"go.uber.org/multierr"
	"moul.io/http2curl"

	"github.com/projectdiscovery/fastdialer/fastdialer"
	"github.com/projectdiscovery/gologger"
	"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
	fuzzStats "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
	"github.com/projectdiscovery/nuclei/v3/pkg/operators"
	"github.com/projectdiscovery/nuclei/v3/pkg/output"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
	"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/helpers/eventcreator"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
	"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/http/httputils"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/signer"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/signerpool"
	templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
	"github.com/projectdiscovery/nuclei/v3/pkg/types"
	"github.com/projectdiscovery/nuclei/v3/pkg/types/nucleierr"
	"github.com/projectdiscovery/rawhttp"
	"github.com/projectdiscovery/retryablehttp-go"
	convUtil "github.com/projectdiscovery/utils/conversion"
	"github.com/projectdiscovery/utils/errkit"
	httpUtils "github.com/projectdiscovery/utils/http"
	"github.com/projectdiscovery/utils/reader"
	sliceutil "github.com/projectdiscovery/utils/slice"
	stringsutil "github.com/projectdiscovery/utils/strings"
	unitutils "github.com/projectdiscovery/utils/unit"
	urlutil "github.com/projectdiscovery/utils/url"
)

const (
	defaultMaxWorkers = 150
	// max unique errors to store & combine
	// when executing requests in parallel
	maxErrorsWhenParallel = 3
)

var (
	MaxBodyRead = 10 * unitutils.Mega
	// ErrMissingVars is error occurred when variables are missing
	ErrMissingVars = errkit.New("stop execution due to unresolved variables").SetKind(nucleierr.ErrTemplateLogic).Build()
	// ErrHttpEngineRequestDeadline is error occurred when request deadline set by http request engine is exceeded
	ErrHttpEngineRequestDeadline = errkit.New("http request engine deadline exceeded").SetKind(errkit.ErrKindDeadline).Build()
)

// Type returns the type of the protocol request
func (request *Request) Type() templateTypes.ProtocolType {
	return templateTypes.HTTPProtocol
}

// rateLimitTake handles rate limiting, using per-host rate limiter if enabled, otherwise global
func (request *Request) rateLimitTake(hostname string) {
	if request.options.Options.PerHostRateLimit && hostname != "" {
		// Use per-host rate limiter
		if limiter, err := httpclientpool.GetPerHostRateLimiter(request.options.Options, hostname); err == nil && limiter != nil {
			limiter.Take()
			// Record request for pps stats
			httpclientpool.RecordPerHostRateLimitRequest(request.options.Options, hostname)
			return
		}
		// Fallback to global if per-host fails
	}
	// Use global rate limiter (or unlimited if per-host is enabled but hostname is empty)
	request.options.RateLimitTake()
}

// executeRaceRequest executes race condition request for a URL
func (request *Request) executeRaceRequest(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
	reqURL := input.MetaInput.Input
	var generatedRequests []*generatedRequest

	// Requests within race condition should be dumped once and the output prefilled to allow DSL language to work
	// This will introduce a delay and will populate in hacky way the field "request" of outputEvent
	generator := request.newGenerator(false)

	inputData, payloads, ok := generator.nextValue()
	if !ok {
		return nil
	}
	ctx := request.newContext(input)
	requestForDump, err := generator.Make(ctx, input, inputData, payloads, dynamicValues)
	if err != nil {
		return err
	}
	request.setCustomHeaders(requestForDump)
	dumpedRequest, err := dump(requestForDump, reqURL)
	if err != nil {
		return err
	}
	if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.StoreResponse {
		msg := fmt.Sprintf("[%s] Dumped HTTP request for %s\n\n", request.options.TemplateID, reqURL)
		if request.options.Options.Debug || request.options.Options.DebugRequests {
			gologger.Info().Msg(msg)
			gologger.Print().Msgf("%s", string(dumpedRequest))
		}
		if request.options.Options.StoreResponse {
			request.options.Output.WriteStoreDebugData(reqURL, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s\n%s", msg, dumpedRequest))
		}
	}
	previous["request"] = string(dumpedRequest)

	// Pre-Generate requests
	for i := 0; i < request.RaceNumberRequests; i++ {
		generator := request.newGenerator(false)
		inputData, payloads, ok := generator.nextValue()
		if !ok {
			break
		}
		ctx := request.newContext(input)
		generatedRequest, err := generator.Make(ctx, input, inputData, payloads, dynamicValues)
		if err != nil {
			return err
		}
		generatedRequests = append(generatedRequests, generatedRequest)
	}

	shouldStop := (request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch || request.options.StopAtFirstMatch)

	childCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	spmHandler := httputils.NewNonBlockingSPMHandler[error](childCtx, maxErrorsWhenParallel, shouldStop)
	defer spmHandler.Cancel()

	gotMatches := &atomic.Bool{}
	// wrappedCallback is a callback that wraps the original callback
	// to implement stop at first match logic
	wrappedCallback := func(event *output.InternalWrappedEvent) {
		if !event.HasOperatorResult() {
			callback(event) // not required but we can allow it
			return
		}
		// this will execute match condition such that if stop at first match is enabled
		// this will be only executed once
		spmHandler.MatchCallback(func() {
			gotMatches.Store(true)
			callback(event)
		})
		if shouldStop {
			// stop all running requests and exit
			spmHandler.Trigger()
		}
	}

	for i := 0; i < request.RaceNumberRequests; i++ {
		updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedRequests[i].URL())
		if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(updatedInput) {
			// stop sending more requests condition is met
			break
		}
		spmHandler.Acquire()
		// execute http request
		go func(httpRequest *generatedRequest, requestInput *contextargs.Context) {
			defer spmHandler.Release()
			if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(requestInput) {
				// stop sending more requests condition is met
				return
			}

			err := request.executeRequest(requestInput, httpRequest, previous, false, wrappedCallback, 0)
			select {
			case <-spmHandler.Done():
				return
			case spmHandler.ResultChan <- err:
				request.recordHostResultAndCancelIfUnresponsive(requestInput, err, spmHandler.Cancel)
				return
			}
		}(generatedRequests[i], updatedInput)
		request.options.Progress.IncrementRequests()
	}
	spmHandler.Wait()

	if spmHandler.FoundFirstMatch() {
		// ignore any context cancellation and in-transit execution errors
		return nil
	}
	return multierr.Combine(spmHandler.CombinedResults()...)
}

// executeParallelHTTP executes parallel requests for a template
func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicValues output.InternalEvent, callback protocols.OutputEventCallback) error {
	// Workers that keeps enqueuing new requests
	maxWorkers := request.Threads

	// if request threads matches global payload concurrency we follow it
	shouldFollowGlobal := maxWorkers == request.options.Options.PayloadConcurrency

	if protocolstate.IsLowOnMemory() {
		maxWorkers = protocolstate.GuardThreadsOrDefault(request.Threads)
	}

	// Stop-at-first-match logic while executing requests
	// parallelly using threads
	shouldStop := (request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch || request.options.StopAtFirstMatch)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	spmHandler := httputils.NewBlockingSPMHandler[error](ctx, maxWorkers, maxErrorsWhenParallel, shouldStop)
	defer spmHandler.Cancel()

	// wrappedCallback is a callback that wraps the original callback
	// to implement stop at first match logic
	wrappedCallback := func(event *output.InternalWrappedEvent) {
		if !event.HasOperatorResult() {
			callback(event) // not required but we can allow it
			return
		}
		// this will execute match condition such that if stop at first match is enabled
		// this will be only executed once
		spmHandler.MatchCallback(func() {
			callback(event)
		})
		if shouldStop {
			// stop all running requests and exit
			spmHandler.Trigger()
		}
	}

	// bounded worker-pool to avoid spawning one goroutine per payload
	type task struct {
		req                *generatedRequest
		updatedInput       *contextargs.Context
		hasInteractMarkers bool
	}

	var workersWg sync.WaitGroup
	currentWorkers := maxWorkers
	tasks := make(chan task, maxWorkers)
	spawnWorker := func(ctx context.Context) {
		workersWg.Add(1)
		go func() {
			defer workersWg.Done()
			for t := range tasks {
				select {
				case <-ctx.Done():
					return
				default:
				}
				if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(t.updatedInput) || spmHandler.Cancelled() {
					continue
				}
				spmHandler.Acquire()
				if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(t.updatedInput) || spmHandler.Cancelled() {
					spmHandler.Release()
					continue
				}
				// Extract hostname for per-host rate limiting (use full URL - normalization happens in rateLimitTake)
				hostname := t.updatedInput.MetaInput.Input
				if t.req != nil && t.req.URL() != "" {
					hostname = t.req.URL()
				} else if t.req != nil && t.req.request != nil && t.req.request.Request != nil && t.req.request.Request.URL != nil {
					// Extract from request URL if available
					hostname = t.req.request.Request.URL.String()
				}
				request.rateLimitTake(hostname)
				hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
				needsRequestEvent := hasInteractMatchers && request.NeedsRequestCondition()
				err := request.executeRequest(t.updatedInput, t.req, make(map[string]interface{}), hasInteractMatchers, func(event *output.InternalWrappedEvent) {
					if (t.hasInteractMarkers || needsRequestEvent) && request.options.Interactsh != nil {
						requestData := &interactsh.RequestData{
							MakeResultFunc: request.MakeResultEvent,
							Event:          event,
							Operators:      request.CompiledOperators,
							MatchFunc:      request.Match,
							ExtractFunc:    request.Extract,
						}
						allOASTUrls := httputils.GetInteractshURLSFromEvent(event.InternalEvent)
						allOASTUrls = append(allOASTUrls, t.req.interactshURLs...)
						request.options.Interactsh.RequestEvent(sliceutil.Dedupe(allOASTUrls), requestData)
					}
					wrappedCallback(event)
				}, 0)
				select {
				case <-spmHandler.Done():
					spmHandler.Release()
					continue
				case spmHandler.ResultChan <- err:
					request.recordHostResultAndCancelIfUnresponsive(t.updatedInput, err, spmHandler.Cancel)
					spmHandler.Release()
				}
			}
		}()
	}
	for i := 0; i < currentWorkers; i++ {
		spawnWorker(ctx)
	}

	// iterate payloads and make requests
	generator := request.newGenerator(false)
	for {
		inputData, payloads, ok := generator.nextValue()
		if !ok {
			break
		}

		select {
		case <-input.Context().Done():
			close(tasks)
			workersWg.Wait()
			return input.Context().Err()
		default:
		}

		// 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 {
				close(tasks)
				workersWg.Wait()
				return err
			}
			// if payload concurrency increased, add more workers
			if spmHandler.Size() > currentWorkers {
				for i := 0; i < spmHandler.Size()-currentWorkers; i++ {
					spawnWorker(ctx)
				}
				currentWorkers = spmHandler.Size()
			}
		}

		// break if stop at first match is found or host is unresponsive
		if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(input) {
			break
		}

		ctx := request.newContext(input)
		generatedHttpRequest, err := generator.Make(ctx, input, inputData, payloads, dynamicValues)
		if err != nil {
			if err == types.ErrNoMoreRequests {
				break
			}
			request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
			close(tasks)
			workersWg.Wait()
			return err
		}
		hasInteractMarkers := interactsh.HasMarkers(inputData) || len(generatedHttpRequest.interactshURLs) > 0
		if input.MetaInput.Input == "" {
			input.MetaInput.Input = generatedHttpRequest.URL()
		}
		updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedHttpRequest.URL())
		if request.isUnresponsiveAddress(updatedInput) {
			// skip on unresponsive host no need to continue
			spmHandler.Cancel()
			close(tasks)
			workersWg.Wait()
			return nil
		}
		select {
		case <-spmHandler.Done():
			close(tasks)
			workersWg.Wait()
			spmHandler.Wait()
			if spmHandler.FoundFirstMatch() {
				return nil
			}
			return multierr.Combine(spmHandler.CombinedResults()...)
		case tasks <- task{req: generatedHttpRequest, updatedInput: updatedInput, hasInteractMarkers: hasInteractMarkers}:
		}
		request.options.Progress.IncrementRequests()
	}
	close(tasks)
	workersWg.Wait()
	spmHandler.Wait()
	if spmHandler.FoundFirstMatch() {
		// ignore any context cancellation and in-transit execution errors
		return nil
	}
	return multierr.Combine(spmHandler.CombinedResults()...)
}

// executeTurboHTTP executes turbo http request for a URL
func (request *Request) executeTurboHTTP(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
	generator := request.newGenerator(false)

	// need to extract the target from the url
	URL, err := urlutil.Parse(input.MetaInput.Input)
	if err != nil {
		return err
	}

	pipeOptions := rawhttp.DefaultPipelineOptions
	pipeOptions.Host = URL.Host
	pipeOptions.MaxConnections = 1
	if request.PipelineConcurrentConnections > 0 {
		pipeOptions.MaxConnections = request.PipelineConcurrentConnections
	}
	if request.PipelineRequestsPerConnection > 0 {
		pipeOptions.MaxPendingRequests = request.PipelineRequestsPerConnection
	}
	pipeClient := rawhttp.NewPipelineClient(pipeOptions)

	// defaultMaxWorkers should be a sufficient value to keep queues always full
	// in case the queue is bigger increase the workers
	maxWorkers := max(pipeOptions.MaxPendingRequests, defaultMaxWorkers)

	// Stop-at-first-match logic while executing requests
	// parallelly using threads
	shouldStop := (request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch || request.options.StopAtFirstMatch)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	spmHandler := httputils.NewBlockingSPMHandler[error](ctx, maxWorkers, maxErrorsWhenParallel, shouldStop)
	defer spmHandler.Cancel()

	// wrappedCallback is a callback that wraps the original callback
	// to implement stop at first match logic
	wrappedCallback := func(event *output.InternalWrappedEvent) {
		if !event.HasOperatorResult() {
			callback(event) // not required but we can allow it
			return
		}
		// this will execute match condition such that if stop at first match is enabled
		// this will be only executed once
		spmHandler.MatchCallback(func() {
			callback(event)
		})
		if shouldStop {
			// stop all running requests and exit
			spmHandler.Trigger()
		}
	}

	for {
		inputData, payloads, ok := generator.nextValue()
		if !ok {
			break
		}

		select {
		case <-input.Context().Done():
			return input.Context().Err()
		default:
		}

		if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(input) || spmHandler.Cancelled() {
			// skip if first match is found
			break
		}

		ctx := request.newContext(input)
		generatedHttpRequest, err := generator.Make(ctx, input, inputData, payloads, dynamicValues)
		if err != nil {
			request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
			return err
		}
		if input.MetaInput.Input == "" {
			input.MetaInput.Input = generatedHttpRequest.URL()
		}
		updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedHttpRequest.URL())
		if request.isUnresponsiveAddress(updatedInput) {
			// skip on unresponsive host no need to continue
			spmHandler.Cancel()
			return nil
		}
		generatedHttpRequest.pipelinedClient = pipeClient
		spmHandler.Acquire()
		go func(httpRequest *generatedRequest, requestInput *contextargs.Context) {
			defer spmHandler.Release()
			if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(requestInput) {
				// skip if first match is found
				return
			}
			err := request.executeRequest(requestInput, httpRequest, previous, false, wrappedCallback, 0)
			select {
			case <-spmHandler.Done():
				return
			case spmHandler.ResultChan <- err:
				request.recordHostResultAndCancelIfUnresponsive(requestInput, err, spmHandler.Cancel)
				return
			}
		}(generatedHttpRequest, updatedInput)
		request.options.Progress.IncrementRequests()
	}
	spmHandler.Wait()
	if spmHandler.FoundFirstMatch() {
		// ignore any context cancellation and in-transit execution errors
		return nil
	}
	return multierr.Combine(spmHandler.CombinedResults()...)
}

// ExecuteWithResults executes the final request on a URL
func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
	// verify if pipeline was requested
	if request.Pipeline {
		return request.executeTurboHTTP(input, dynamicValues, previous, callback)
	}
	// verify if a basic race condition was requested
	if request.Race && request.RaceNumberRequests > 0 {
		return request.executeRaceRequest(input, dynamicValues, previous, callback)
	}

	// verify if fuzz elaboration was requested
	if len(request.Fuzzing) > 0 {
		return request.executeFuzzingRule(input, dynamicValues, callback)
	}

	// verify if parallel elaboration was requested
	if request.Threads > 0 && (len(request.Payloads) > 0 || request.Race) {
		return request.executeParallelHTTP(input, dynamicValues, callback)
	}

	generator := request.newGenerator(false)

	var gotDynamicValues map[string][]string
	var requestErr error

	for {
		// returns two values, error and skip, which skips the execution for the request instance.
		executeFunc := func(data string, payloads, dynamicValue map[string]interface{}) (bool, error) {
			hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)

			ctx := request.newContext(input)
			ctxWithTimeout, cancel := context.WithTimeoutCause(ctx, request.options.Options.GetTimeouts().HttpTimeout, ErrHttpEngineRequestDeadline)
			defer cancel()

			generatedHttpRequest, err := generator.Make(ctxWithTimeout, input, data, payloads, dynamicValue)
			if err != nil {
				if err == types.ErrNoMoreRequests {
					return true, nil
				}
				return true, err
			}
			// ideally if http template used a custom port or hostname
			// we would want to update it in input but currently templateCtx logic
			// is closely tied to contextargs.Context so we are temporarily creating
			// 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())

			// Extract hostname for per-host rate limiting (use generated request URL - normalization happens in rateLimitTake)
			hostname := input.MetaInput.Input
			if generatedHttpRequest.URL() != "" {
				// Use the generated URL directly - the normalization function will extract host:port correctly
				hostname = generatedHttpRequest.URL()
			}
			request.rateLimitTake(hostname)

			if generatedHttpRequest.customCancelFunction != nil {
				defer generatedHttpRequest.customCancelFunction()
			}

			hasInteractMarkers := interactsh.HasMarkers(data) || len(generatedHttpRequest.interactshURLs) > 0
			if input.MetaInput.Input == "" {
				input.MetaInput.Input = generatedHttpRequest.URL()
			}
			// Check if hosts keep erroring
			if request.isUnresponsiveAddress(updatedInput) {
				return true, nil
			}
			var gotMatches bool
			execReqErr := request.executeRequest(input, generatedHttpRequest, previous, hasInteractMatchers, func(event *output.InternalWrappedEvent) {
				// a special case where operators has interactsh matchers and multiple request are made
				// ex: status_code_2 , interactsh_protocol (from 1st request) etc
				needsRequestEvent := interactsh.HasMatchers(request.CompiledOperators) && request.NeedsRequestCondition()
				if (hasInteractMarkers || needsRequestEvent) && request.options.Interactsh != nil {
					requestData := &interactsh.RequestData{
						MakeResultFunc: request.MakeResultEvent,
						Event:          event,
						Operators:      request.CompiledOperators,
						MatchFunc:      request.Match,
						ExtractFunc:    request.Extract,
					}
					allOASTUrls := httputils.GetInteractshURLSFromEvent(event.InternalEvent)
					allOASTUrls = append(allOASTUrls, generatedHttpRequest.interactshURLs...)
					request.options.Interactsh.RequestEvent(sliceutil.Dedupe(allOASTUrls), requestData)
					gotMatches = request.options.Interactsh.AlreadyMatched(requestData)
				}
				// Add the extracts to the dynamic values if any.
				if event.OperatorsResult != nil {
					gotMatches = event.OperatorsResult.Matched
					gotDynamicValues = generators.MergeMapsMany(event.OperatorsResult.DynamicValues, dynamicValues, gotDynamicValues)
				}
				// Note: This is a race condition prone zone i.e when request has interactsh_matchers
				// Interactsh.RequestEvent tries to access/update output.InternalWrappedEvent depending on logic
				// to avoid conflicts with `callback` mutex is used here and in Interactsh.RequestEvent
				// Note: this only happens if requests > 1 and interactsh matcher is used
				// TODO: interactsh logic in nuclei needs to be refactored to avoid such situations
				callback(event)
			}, generator.currentIndex)

			// If a variable is unresolved, skip all further requests
			if errors.Is(execReqErr, ErrMissingVars) {
				return true, nil
			}

			request.recordHostResult(updatedInput, execReqErr)
			if execReqErr != nil {
				reqKitErr := errkit.FromError(execReqErr)
				reqKitErr.Msgf("got err while executing %v", generatedHttpRequest.URL())

				requestErr = reqKitErr
				request.options.Progress.IncrementFailedRequestsBy(1)
			} else {
				request.options.Progress.IncrementRequests()
			}

			// If this was a match, and we want to stop at first match, skip all further requests.
			shouldStopAtFirstMatch := generatedHttpRequest.original.options.Options.StopAtFirstMatch || generatedHttpRequest.original.options.StopAtFirstMatch || request.StopAtFirstMatch
			if shouldStopAtFirstMatch && gotMatches {
				return true, nil
			}
			return false, nil
		}

		inputData, payloads, ok := generator.nextValue()
		if !ok {
			break
		}

		select {
		case <-input.Context().Done():
			return input.Context().Err()
		default:
		}

		var gotErr error
		var skip bool
		if len(gotDynamicValues) > 0 {
			operators.MakeDynamicValuesCallback(gotDynamicValues, request.IterateAll, func(data map[string]interface{}) bool {
				if skip, gotErr = executeFunc(inputData, payloads, data); skip || gotErr != nil {
					return true
				}
				return false
			})
		} else {
			skip, gotErr = executeFunc(inputData, payloads, dynamicValues)
		}
		if gotErr != nil && requestErr == nil {
			requestErr = gotErr
		}
		if skip || gotErr != nil {
			request.options.Progress.SetRequests(uint64(generator.Remaining() + 1))
			break
		}
	}
	return requestErr
}

const drainReqSize = int64(8 * unitutils.Kilo)

// executeRequest executes the actual generated request and returns error if occurred
func (request *Request) executeRequest(input *contextargs.Context, generatedRequest *generatedRequest, previousEvent output.InternalEvent, hasInteractMatchers bool, processEvent protocols.OutputEventCallback, requestCount int) (err error) {
	// Check if hosts keep erroring
	if request.isUnresponsiveAddress(input) {
		return fmt.Errorf("hostErrorsCache : host %s is unresponsive", input.MetaInput.Input)
	}

	// wrap one more callback for validation and fixing event
	callback := func(event *output.InternalWrappedEvent) {
		// validateNFixEvent performs necessary validation on generated event
		// and attempts to fix it , this includes things like making sure
		// `template-id` is set , `request-url-pattern` is set etc
		request.validateNFixEvent(input, generatedRequest, err, event)
		processEvent(event)
	}

	request.setCustomHeaders(generatedRequest)

	var (
		resp            *http.Response
		fromCache       bool
		dumpedRequest   []byte
		projectCacheKey []byte
		// executingClient is the client that actually performed the HTTP
		// request, preserving any per-request overrides (cookie jar,
		// CustomMaxTimeout) applied via connConfig.Clone(). Reused below by
		// the analyzer so follow-up requests share the same session/timeout.
		executingClient *retryablehttp.Client
	)

	// Dump request for variables checks
	// For race conditions we can't dump the request body at this point as it's already waiting the open-gate event, already handled with a similar code within the race function
	if !generatedRequest.original.Race {

		// change encoding type to content-length unless transfer-encoding header is manually set
		if generatedRequest.request != nil && !stringsutil.EqualFoldAny(generatedRequest.request.Method, http.MethodGet, http.MethodHead) && generatedRequest.request.Body != nil && generatedRequest.request.Header.Get("Transfer-Encoding") != "chunked" {
			var newReqBody *reader.ReusableReadCloser
			newReqBody, ok := generatedRequest.request.Body.(*reader.ReusableReadCloser)
			if !ok {
				newReqBody, err = reader.NewReusableReadCloser(generatedRequest.request.Body)
			}
			if err == nil {
				// update the request body with the reusable reader
				generatedRequest.request.SetBodyReader(newReqBody)
				// get content length
				length, _ := io.Copy(io.Discard, newReqBody)
				generatedRequest.request.ContentLength = length
			} else {
				// log error and continue
				gologger.Verbose().Msgf("[%v] Could not read request body while forcing transfer encoding: %s\n", request.options.TemplateID, err)
				err = nil
			}
		}

		// do the same for unsafe requests
		if generatedRequest.rawRequest != nil && !stringsutil.EqualFoldAny(generatedRequest.rawRequest.Method, http.MethodGet, http.MethodHead) && generatedRequest.rawRequest.Data != "" && generatedRequest.rawRequest.Headers["Transfer-Encoding"] != "chunked" {
			generatedRequest.rawRequest.Headers["Content-Length"] = strconv.Itoa(len(generatedRequest.rawRequest.Data))
		}

		var dumpError error
		// TODO: dump is currently not working with post-processors - somehow it alters the signature
		dumpedRequest, dumpError = dump(generatedRequest, input.MetaInput.Input)
		if dumpError != nil {
			return dumpError
		}
		if generatedRequest.request != nil && generatedRequest.request.Request != nil && generatedRequest.request.Request.URL != nil {
			projectCacheKey = getHTTPProjectCacheScope(dumpedRequest, generatedRequest.request.Scheme, generatedRequest.request.Request.URL.Host)
		} else {
			projectCacheKey = dumpedRequest
		}
		dumpedRequestString := string(dumpedRequest)

		if ignoreList := GetVariablesNamesSkipList(generatedRequest.original.Signature.Value); ignoreList != nil {
			if varErr := expressions.ContainsVariablesWithIgnoreList(ignoreList, dumpedRequestString); varErr != nil && !request.SkipVariablesCheck {
				gologger.Warning().Msgf("[%s] Could not make http request for %s: %v\n", request.options.TemplateID, input.MetaInput.Input, varErr)
				return ErrMissingVars
			}
		} else { // Check if are there any unresolved variables. If yes, skip unless overridden by user.
			if varErr := expressions.ContainsUnresolvedVariables(dumpedRequestString); varErr != nil && !request.SkipVariablesCheck {
				gologger.Warning().Msgf("[%s] Could not make http request for %s: %v\n", request.options.TemplateID, input.MetaInput.Input, varErr)
				return ErrMissingVars
			}
		}
	}

	// === apply auth strategies ===
	if generatedRequest.request != nil && !request.SkipSecretFile {
		generatedRequest.ApplyAuth(request.options.AuthProvider)
	}

	var formedURL string
	var hostname string
	timeStart := time.Now()
	if generatedRequest.original.Pipeline {
		// if request is a pipeline request, use the pipelined client
		if generatedRequest.rawRequest != nil {
			formedURL = generatedRequest.rawRequest.FullURL
			if parsed, parseErr := urlutil.ParseURL(formedURL, true); parseErr == nil {
				hostname = parsed.Host
			}
			resp, err = generatedRequest.pipelinedClient.DoRaw(generatedRequest.rawRequest.Method, input.MetaInput.Input, generatedRequest.rawRequest.Path, generators.ExpandMapValues(generatedRequest.rawRequest.Headers), io.NopCloser(strings.NewReader(generatedRequest.rawRequest.Data)))
		} else if generatedRequest.request != nil {
			resp, err = generatedRequest.pipelinedClient.Dor(generatedRequest.request)
		}
	} else if generatedRequest.original.Unsafe && generatedRequest.rawRequest != nil {
		// if request is a unsafe request, use the rawhttp client
		formedURL = generatedRequest.rawRequest.FullURL
		// use request url as matched url if empty
		if formedURL == "" {
			urlx, err := urlutil.Parse(input.MetaInput.Input)
			if err != nil {
				formedURL = fmt.Sprintf("%s%s", input.MetaInput.Input, generatedRequest.rawRequest.Path)
			} else {
				_ = urlx.MergePath(generatedRequest.rawRequest.Path, true)
				formedURL = urlx.String()
			}
		}
		if parsed, parseErr := urlutil.ParseURL(formedURL, true); parseErr == nil {
			hostname = parsed.Host
		}
		options := *generatedRequest.original.rawhttpClient.Options
		options.FollowRedirects = request.Redirects
		options.CustomRawBytes = generatedRequest.rawRequest.UnsafeRawBytes
		options.ForceReadAllBody = request.ForceReadAllBody
		options.SNI = request.options.Options.SNI
		inputUrl := input.MetaInput.Input
		if generatedRequest.rawRequest.FullURL != "" {
			inputUrl = generatedRequest.rawRequest.FullURL
		}
		if url, err := urlutil.ParseURL(inputUrl, false); err == nil {
			url.Path = ""
			url.Params = urlutil.NewOrderedParams() // donot include query params
			// inputUrl should only contain scheme://host:port
			inputUrl = url.String()
		}
		formedURL = fmt.Sprintf("%s%s", inputUrl, generatedRequest.rawRequest.Path)

		// send rawhttp request and get response
		resp, err = httpclientpool.SendRawRequest(generatedRequest.original.rawhttpClient, &httpclientpool.RawHttpRequestOpts{
			Method:  generatedRequest.rawRequest.Method,
			URL:     inputUrl,
			Path:    generatedRequest.rawRequest.Path,
			Headers: generators.ExpandMapValues(generatedRequest.rawRequest.Headers),
			Body:    io.NopCloser(strings.NewReader(generatedRequest.rawRequest.Data)),
			Options: &options,
		})
	} else {
		//** For Normal requests **//
		// Use the dial target (URL.Host) rather than the optional Host-header
		// override (request.Host), so the per-host pool keys distinct
		// connection targets even when templates set a custom Host header
		// against multiple IPs/vhosts.
		if generatedRequest.request.URL != nil {
			hostname = generatedRequest.request.URL.Host
		} else {
			hostname = generatedRequest.request.Host
		}
		formedURL = generatedRequest.request.String()
		// if nuclei-project is available check if the request was already sent previously
		if request.options.ProjectFile != nil {
			// if unavailable fail silently
			fromCache = true
			resp, err = request.options.ProjectFile.Get(projectCacheKey)
			if err != nil {
				fromCache = false
			}
		}
		if resp == nil {
			if errSignature := request.handleSignature(generatedRequest); errSignature != nil {
				return errSignature
			}

			connConfig := request.connConfiguration
			if input.CookieJar != nil {
				connConfig = connConfig.Clone()
				connConfig.Connection.SetCookieJar(input.CookieJar)
			}
			if updatedTimeout, ok := generatedRequest.request.Context().Value(httpclientpool.WithCustomTimeout{}).(httpclientpool.WithCustomTimeout); ok {
				if connConfig == request.connConfiguration {
					connConfig = connConfig.Clone()
				}
				connConfig.ResponseHeaderTimeout = updatedTimeout.Timeout
			}

			httpclient, clientErr := httpclientpool.Get(request.options.Options, connConfig, hostname)
			if clientErr != nil {
				return errors.Wrap(clientErr, "could not get http client")
			}
			executingClient = httpclient

			// Check if HTTP-to-HTTPS port correction is needed before sending request.
			// The correction is keyed by host:port and shared across templates, so a
			// single wrong detection could otherwise silently break every later request
			// to that host. We remember that a correction was applied so we can revert
			// and retry on failure (see below).
			var httpsCorrectionTracker *httpclientpool.HTTPToHTTPSPortTracker
			var httpsCorrectionURL string
			if generatedRequest.request != nil && generatedRequest.request.Request != nil && generatedRequest.request.Request.URL != nil {
				tracker := httpclientpool.GetHTTPToHTTPSPortTracker(request.options.Options)
				if tracker != nil {
					requestURL := generatedRequest.request.Request.URL.String()
					if tracker.RequiresHTTPS(requestURL) {
						// Modify request URL scheme from http to https
						if generatedRequest.request.Scheme == "http" {
							generatedRequest.request.Scheme = "https"
							tracker.RecordCorrection()
							httpsCorrectionTracker = tracker
							httpsCorrectionURL = requestURL
							gologger.Debug().Msgf("[http-to-https-tracker] Corrected HTTP to HTTPS for %s", requestURL)
						}
					}
				}
			}

			resp, err = httpclient.Do(generatedRequest.request)

			// If we forced http->https from a previous detection and the corrected
			// request failed (e.g. a false positive where the port actually speaks
			// plain HTTP), revert to the original scheme, evict the bad entry so
			// other templates hitting the same host:port are not affected, and retry
			// once. This keeps the optimization while preventing a single
			// wrong detection from silently dropping findings at scale.
			if err != nil && httpsCorrectionTracker != nil && generatedRequest.request != nil && generatedRequest.request.Scheme == "https" {
				generatedRequest.request.Scheme = "http"
				httpsCorrectionTracker.Evict(httpsCorrectionURL)
				resp, err = httpclient.Do(generatedRequest.request)
			}
		}
	}
	// use request url as matched url if empty
	if formedURL == "" {
		formedURL = input.MetaInput.Input
	}

	// converts whitespace and other chars that cannot be printed to url encoded values
	formedURL = urlutil.URLEncodeWithEscapes(formedURL)

	// Dump the requests containing all headers
	if !generatedRequest.original.Race {
		var dumpError error
		dumpedRequest, dumpError = dump(generatedRequest, input.MetaInput.Input)
		if dumpError != nil {
			return dumpError
		}
		dumpedRequestString := string(dumpedRequest)
		if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.StoreResponse {
			msg := fmt.Sprintf("[%s] Dumped HTTP request for %s\n\n", request.options.TemplateID, formedURL)

			if request.options.Options.Debug || request.options.Options.DebugRequests {
				gologger.Info().Msg(msg)
				gologger.Print().Msgf("%s", dumpedRequestString)
			}
			if request.options.Options.StoreResponse {
				request.options.Output.WriteStoreDebugData(input.MetaInput.Input, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s\n%s", msg, dumpedRequestString))
			}
		}
	}

	dialers := protocolstate.GetDialersWithId(request.options.Options.ExecutionId)
	if dialers == nil {
		return fmt.Errorf("dialers not found for execution id %s", request.options.Options.ExecutionId)
	}

	if err != nil {
		// rawhttp doesn't support draining response bodies.
		if resp != nil && resp.Body != nil && generatedRequest.rawRequest == nil && !generatedRequest.original.Pipeline {
			_, _ = io.CopyN(io.Discard, resp.Body, drainReqSize)
			_ = resp.Body.Close()
		}
		request.options.Output.Request(request.options.TemplatePath, formedURL, request.Type().String(), err)
		request.options.Progress.IncrementErrorsBy(1)

		// In case of interactsh markers and request times out, still send
		// a callback event so in case we receive an interaction, correlation is possible.
		// Also, to log failed use-cases.
		outputEvent := request.responseToDSLMap(&http.Response{}, input.MetaInput.Input, formedURL, convUtil.String(dumpedRequest), "", "", "", 0, generatedRequest.meta)
		if i := strings.LastIndex(hostname, ":"); i != -1 {
			hostname = hostname[:i]
		}

		if input.MetaInput.CustomIP != "" {
			outputEvent["ip"] = input.MetaInput.CustomIP
		} else {
			outputEvent["ip"] = dialers.Fastdialer.GetDialedIP(hostname)
			// try getting cname
			request.addCNameIfAvailable(hostname, outputEvent)
		}

		if len(generatedRequest.interactshURLs) > 0 {
			// according to logic we only need to trigger a callback if interactsh was used
			// and request failed in hope that later on oast interaction will be received
			event := &output.InternalWrappedEvent{}
			if request.CompiledOperators != nil && request.CompiledOperators.HasDSL() {
				event.InternalEvent = outputEvent
			}
			callback(event)
		}
		return err
	}

	var curlCommand string
	if !request.Unsafe && resp != nil && generatedRequest.request != nil && resp.Request != nil && !request.Race {
		bodyBytes, _ := generatedRequest.request.BodyBytes()
		// Use a clone to avoid a race condition with the http transport
		req := resp.Request.Clone(resp.Request.Context())
		req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
		command, err := http2curl.GetCurlCommand(req)
		if err == nil && command != nil {
			curlCommand = command.String()
		}
	}

	gologger.Verbose().Msgf("[%s] Sent HTTP request to %s", request.options.TemplateID, formedURL)
	request.options.Output.Request(request.options.TemplatePath, formedURL, request.Type().String(), err)

	duration := time.Since(timeStart)

	// define max body read limit
	maxBodylimit := MaxBodyRead // 10MB
	if request.MaxSize > 0 {
		maxBodylimit = request.MaxSize
	}
	if request.options.Options.ResponseReadSize != 0 {
		maxBodylimit = request.options.Options.ResponseReadSize
	}

	// respChain is http response chain that reads response body
	// efficiently by reusing buffers and does all decoding and optimizations
	respChain := httpUtils.NewResponseChain(resp, int64(maxBodylimit))
	defer respChain.Close() // reuse buffers

	// we only intend to log/save the final redirected response
	// i.e why we have to use sync.Once to ensure it's only done once
	var errx error
	onceFunc := sync.OnceFunc(func() {
		// if nuclei-project is enabled store the response if not previously done
		if request.options.ProjectFile != nil && !fromCache {
			if err := request.options.ProjectFile.Set(projectCacheKey, resp, respChain.BodyBytes()); err != nil {
				errx = errors.Wrap(err, "could not store in project file")
			}
		}
	})

	// evaluate responses continuously until first redirect request in reverse order
	for respChain.Has() {
		// fill buffers, read response body and reuse connection
		if err := respChain.Fill(); err != nil {
			return errors.Wrap(err, "could not generate response chain")
		}

		// Cache response strings once per Fill() to avoid repeated allocs.
		// NOTE(dwisiswant0): These are valid until Previous() (which reloads
		// the buffer).
		fullResponseStr := respChain.FullResponseString()
		bodyStr := respChain.BodyString()
		headersStr := respChain.HeadersString()

		statusCode := respChain.Response().StatusCode

		// Detect HTTP-to-HTTPS port mismatch (400 error with specific message) so
		// later requests to the same host:port are auto-upgraded to https.
		if statusCode == 400 && strings.Contains(bodyStr, "The plain HTTP request was sent to HTTPS port") {
			var requestURL string
			if generatedRequest.request != nil && generatedRequest.request.Request != nil && generatedRequest.request.Request.URL != nil {
				requestURL = generatedRequest.request.Request.URL.String()
			} else if generatedRequest.rawRequest != nil && generatedRequest.rawRequest.FullURL != "" {
				requestURL = generatedRequest.rawRequest.FullURL
			} else if respChain.Request() != nil && respChain.Request().URL != nil {
				requestURL = respChain.Request().URL.String()
			}
			if requestURL != "" {
				httpclientpool.RecordHTTPToHTTPSPortMismatch(request.options.Options, requestURL)
			}
		}

		// log request stats
		request.options.Output.RequestStatsLog(strconv.Itoa(statusCode), fullResponseStr)

		// save response to projectfile
		onceFunc()
		matchedURL := input.MetaInput.Input
		if generatedRequest.rawRequest != nil {
			if generatedRequest.rawRequest.FullURL != "" {
				matchedURL = generatedRequest.rawRequest.FullURL
			} else {
				matchedURL = formedURL
			}
		}
		if generatedRequest.request != nil {
			matchedURL = generatedRequest.request.String()
		}
		// Give precedence to the final URL from response
		if respChain.Request() != nil {
			if responseURL := respChain.Request().URL.String(); responseURL != "" {
				matchedURL = responseURL
			}
		}

		finalEvent := make(output.InternalEvent)

		if request.Analyzer != nil {
			analyzer := analyzers.GetAnalyzer(request.Analyzer.Name)
			// Prefer reusing the exact client that executed the request so
			// the analyzer inherits any per-request cookie jar / timeout
			// overrides; fall back to a per-host lookup for paths that did
			// not go through the standard execution flow (pipeline/unsafe).
			analyzerClient := executingClient
			if analyzerClient == nil {
				analyzerClient = request.getHTTPClientForHost(hostname)
			}
			if analyzerClient == nil {
				gologger.Warning().Msgf("Could not get http client for analyzer %s on %s, skipping analysis\n", request.Analyzer.Name, hostname)
			} else {
				analysisMatched, analysisDetails, err := analyzer.Analyze(&analyzers.Options{
					FuzzGenerated:      generatedRequest.fuzzGeneratedRequest,
					HttpClient:         analyzerClient,
					ResponseTimeDelay:  duration,
					AnalyzerParameters: request.Analyzer.Parameters,
				})
				if err != nil {
					gologger.Warning().Msgf("Could not analyze response: %v\n", err)
				}
				if analysisMatched {
					finalEvent["analyzer_details"] = analysisDetails
					finalEvent["analyzer"] = true
				}
			}
		}

		outputEvent := request.responseToDSLMap(respChain.Response(), input.MetaInput.Input, matchedURL, convUtil.String(dumpedRequest), fullResponseStr, bodyStr, headersStr, duration, generatedRequest.meta)
		// add response fields to template context and merge templatectx variables to output event
		request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, outputEvent)
		if request.options.HasTemplateCtx(input.MetaInput) {
			outputEvent = generators.MergeMaps(outputEvent, request.options.GetTemplateCtx(input.MetaInput).GetAll())
		}
		if i := strings.LastIndex(hostname, ":"); i != -1 {
			hostname = hostname[:i]
		}
		outputEvent["curl-command"] = curlCommand
		if input.MetaInput.CustomIP != "" {
			outputEvent["ip"] = input.MetaInput.CustomIP
		} else {
			dialer := dialers.Fastdialer
			if dialer != nil {
				outputEvent["ip"] = dialer.GetDialedIP(hostname)
			}

			// try getting cname
			request.addCNameIfAvailable(hostname, outputEvent)
		}
		if request.options.Interactsh != nil {
			request.options.Interactsh.MakePlaceholders(generatedRequest.interactshURLs, outputEvent)
		}
		maps.Copy(finalEvent, previousEvent)
		maps.Copy(finalEvent, outputEvent)

		// Add to history the current request number metadata if asked by the user.
		if request.NeedsRequestCondition() {
			for k, v := range outputEvent {
				key := fmt.Sprintf("%s_%d", k, requestCount)
				if previousEvent != nil {
					previousEvent[key] = v
				}
				finalEvent[key] = v
			}
		}
		// prune signature internal values if any
		request.pruneSignatureInternalValues(generatedRequest.meta)

		interimEvent := generators.MergeMaps(generatedRequest.dynamicValues, finalEvent)
		interimEvent["payloads"] = generatedRequest.meta
		// add the request URL pattern to the event BEFORE operators execute
		// so that interactsh events etc can also access it
		if request.options.ExportReqURLPattern {
			interimEvent[ReqURLPatternKey] = generatedRequest.requestURLPattern
		}
		isDebug := request.options.Options.Debug || request.options.Options.DebugResponse
		event := eventcreator.CreateEventWithAdditionalOptions(request, interimEvent, isDebug, func(internalWrappedEvent *output.InternalWrappedEvent) {
			internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
		})

		if hasInteractMatchers {
			event.UsesInteractsh = true
		}

		if request.options.GlobalMatchers.HasMatchers() {
			request.options.GlobalMatchers.Match(interimEvent, request.Match, request.Extract, isDebug, func(event output.InternalEvent, result *operators.Result) {
				callback(eventcreator.CreateEventWithOperatorResults(request, event, result))
			})
		}

		responseContentType := respChain.Response().Header.Get("Content-Type")
		isResponseTruncated := request.MaxSize > 0 && respChain.Body().Len() >= request.MaxSize
		dumpResponse(event, request, fullResponseStr, formedURL, responseContentType, isResponseTruncated, input.MetaInput.Input)

		callback(event)

		if request.options.FuzzStatsDB != nil && generatedRequest.fuzzGeneratedRequest.Request != nil {
			request.options.FuzzStatsDB.RecordResultEvent(fuzzStats.FuzzingEvent{
				URL:           input.MetaInput.Target(),
				TemplateID:    request.options.TemplateID,
				ComponentType: generatedRequest.fuzzGeneratedRequest.Component.Name(),
				ComponentName: generatedRequest.fuzzGeneratedRequest.Parameter,
				PayloadSent:   generatedRequest.fuzzGeneratedRequest.Value,
				StatusCode:    respChain.Response().StatusCode,
				Matched:       event.HasResults(),
				RawRequest:    string(dumpedRequest),
				RawResponse:   fullResponseStr,
				Severity:      request.options.TemplateInfo.SeverityHolder.Severity.String(),
			})
		}

		// Skip further responses if we have stop-at-first-match and a match
		if (request.options.Options.StopAtFirstMatch || request.options.StopAtFirstMatch || request.StopAtFirstMatch) && event.HasResults() {
			return nil
		}
		// proceed with previous response
		// we evaluate operators recursively for each response
		// until we reach the first redirect response
		if !respChain.Previous() {
			break
		}
	}
	// return project file save error if any
	return errx
}

// validateNFixEvent validates and fixes the event
// it adds any missing template-id and request-url-pattern
func (request *Request) validateNFixEvent(input *contextargs.Context, gr *generatedRequest, err error, event *output.InternalWrappedEvent) {
	if event != nil {
		if event.InternalEvent == nil {
			event.InternalEvent = make(map[string]interface{})
			event.InternalEvent["template-id"] = request.options.TemplateID
		}
		// add the request URL pattern to the event
		event.InternalEvent[ReqURLPatternKey] = gr.requestURLPattern
		if event.InternalEvent["host"] == nil {
			event.InternalEvent["host"] = input.MetaInput.Input
		}
		if event.InternalEvent["template-id"] == nil {
			event.InternalEvent["template-id"] = request.options.TemplateID
		}
		if event.InternalEvent["type"] == nil {
			event.InternalEvent["type"] = request.Type().String()
		}
		if event.InternalEvent["template-path"] == nil {
			event.InternalEvent["template-path"] = request.options.TemplatePath
		}
		if event.InternalEvent["template-info"] == nil {
			event.InternalEvent["template-info"] = request.options.TemplateInfo
		}
		if err != nil {
			event.InternalEvent["error"] = err.Error()
		}
	}
}

// getHTTPClientForHost returns a per-host HTTP client, falling back to a
// host-agnostic client if the lookup fails.
func (request *Request) getHTTPClientForHost(host string) *retryablehttp.Client {
	client, err := httpclientpool.Get(request.options.Options, request.connConfiguration, host)
	if err != nil {
		client, _ = httpclientpool.Get(request.options.Options, request.connConfiguration, "")
	}
	return client
}

// addCNameIfAvailable adds the cname to the event if available
func (request *Request) addCNameIfAvailable(hostname string, outputEvent map[string]interface{}) {
	if request.dialer == nil {
		return
	}

	if request.options.Interactsh != nil {
		interactshDomain := request.options.Interactsh.GetHostname()
		if interactshDomain != "" {
			if strings.EqualFold(hostname, interactshDomain) || strings.HasSuffix(hostname, "."+interactshDomain) {
				return
			}
		}
	}

	data, err := request.dialer.GetDNSData(hostname)
	if err == nil {
		switch len(data.CNAME) {
		case 0:
			return
		case 1:
			outputEvent["cname"] = data.CNAME[0]
		default:
			// add 1st and put others in cname_all
			outputEvent["cname"] = data.CNAME[0]
			outputEvent["cname_all"] = data.CNAME
		}
	}
}

// handleSignature of the http request
func (request *Request) handleSignature(generatedRequest *generatedRequest) error {
	switch request.Signature.Value {
	case AWSSignature:
		var awsSigner signer.Signer
		allvars := generators.MergeMaps(request.options.Options.Vars.AsMap(), generatedRequest.dynamicValues)
		awsopts := signer.AWSOptions{
			AwsID:          types.ToString(allvars["aws-id"]),
			AwsSecretToken: types.ToString(allvars["aws-secret"]),
		}
		awsSigner, err := signerpool.Get(request.options.Options, &signerpool.Configuration{SignerArgs: &awsopts})
		if err != nil {
			return err
		}
		ctx := signer.GetCtxWithArgs(allvars, signer.AwsDefaultVars)
		err = awsSigner.SignHTTP(ctx, generatedRequest.request.Request)
		if err != nil {
			return err
		}
	}

	return nil
}

// setCustomHeaders sets the custom headers for generated request
func (request *Request) setCustomHeaders(req *generatedRequest) {
	for k, v := range request.customHeaders {
		if req.rawRequest != nil {
			req.rawRequest.Headers[k] = v
		} else {
			kk, vv := strings.TrimSpace(k), strings.TrimSpace(v)
			// NOTE(dwisiswant0): Do we really not need to convert it first into
			// lowercase?
			if kk == "Host" {
				req.request.Host = vv

				continue
			}

			req.request.Header[kk] = []string{vv}
		}
	}
}

const CRLF = "\r\n"

func dumpResponse(event *output.InternalWrappedEvent, request *Request, redirectedResponse string, formedURL string, responseContentType string, isResponseTruncated bool, reqURL string) {
	cliOptions := request.options.Options
	if cliOptions.Debug || cliOptions.DebugResponse || cliOptions.StoreResponse {
		response := redirectedResponse

		var highlightedResult string
		if (responseContentType == "application/octet-stream" || responseContentType == "application/x-www-form-urlencoded") && responsehighlighter.HasBinaryContent(response) {
			highlightedResult = createResponseHexDump(event, response, cliOptions.NoColor)
		} else {
			highlightedResult = responsehighlighter.Highlight(event.OperatorsResult, response, cliOptions.NoColor, false)
		}

		msg := "[%s] Dumped HTTP response %s\n\n%s"
		if isResponseTruncated {
			msg = "[%s] Dumped HTTP response (Truncated) %s\n\n%s"
		}
		fMsg := fmt.Sprintf(msg, request.options.TemplateID, formedURL, highlightedResult)
		if cliOptions.Debug || cliOptions.DebugResponse {
			gologger.Debug().Msg(fMsg)
		}
		if cliOptions.StoreResponse {
			request.options.Output.WriteStoreDebugData(reqURL, request.options.TemplateID, request.Type().String(), fMsg)
		}
	}
}

func createResponseHexDump(event *output.InternalWrappedEvent, response string, noColor bool) string {
	CRLFs := CRLF + CRLF
	headerEndIndex := strings.Index(response, CRLFs) + len(CRLFs)
	if headerEndIndex > 0 {
		headers := response[0:headerEndIndex]
		responseBodyHexDump := hex.Dump([]byte(response[headerEndIndex:]))

		highlightedHeaders := responsehighlighter.Highlight(event.OperatorsResult, headers, noColor, false)
		highlightedResponse := responsehighlighter.Highlight(event.OperatorsResult, responseBodyHexDump, noColor, true)
		return fmt.Sprintf("%s\n%s", highlightedHeaders, highlightedResponse)
	} else {
		return responsehighlighter.Highlight(event.OperatorsResult, hex.Dump([]byte(response)), noColor, true)
	}
}

func (request *Request) pruneSignatureInternalValues(maps ...map[string]interface{}) {
	var signatureFieldsToSkip map[string]interface{}
	switch request.Signature.Value {
	case AWSSignature:
		signatureFieldsToSkip = signer.AwsInternalOnlyVars
	default:
		return
	}

	for _, m := range maps {
		for fieldName := range signatureFieldsToSkip {
			delete(m, fieldName)
		}
	}
}

func (request *Request) newContext(input *contextargs.Context) context.Context {
	if input.MetaInput.CustomIP != "" {
		return context.WithValue(input.Context(), fastdialer.IP, input.MetaInput.CustomIP)
	}
	return input.Context()
}

// recordHostResult updates the host-errors cache with the outcome of a request.
// A failure is counted; a success resets the host, so only consecutive failures
// with no success in between can mark a host unresponsive.
func (request *Request) recordHostResult(input *contextargs.Context, err error) {
	if request.options.HostErrorsCache != nil {
		request.options.HostErrorsCache.MarkFailedOrRemove(request.options.ProtocolType.String(), input, err)
	}
}

func (request *Request) recordHostResultAndCancelIfUnresponsive(input *contextargs.Context, err error, cancel func()) {
	request.recordHostResult(input, err)
	if request.isUnresponsiveAddress(input) {
		cancel()
	}
}

// isUnresponsiveAddress checks if the error is a unreponsive based on its execution history
func (request *Request) isUnresponsiveAddress(input *contextargs.Context) bool {
	if request.options.HostErrorsCache != nil {
		return request.options.HostErrorsCache.Check(request.options.ProtocolType.String(), input)
	}
	return false
}
