Path: blob/dev/pkg/protocols/headless/engine/page.go
2843 views
package engine12import (3"bufio"4"fmt"5"net/http"6"net/url"7"strings"8"sync"9"time"1011"github.com/go-rod/rod"12"github.com/go-rod/rod/lib/proto"13"github.com/projectdiscovery/gologger"14"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"15"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"16"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"17"github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"18httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"19"github.com/projectdiscovery/nuclei/v3/pkg/types"20"github.com/projectdiscovery/utils/errkit"21urlutil "github.com/projectdiscovery/utils/url"22)2324// Page is a single page in an isolated browser instance25type Page struct {26ctx *contextargs.Context27inputURL *urlutil.URL28options *Options29page *rod.Page30rules []rule31instance *Instance32hijackRouter *rod.HijackRouter33hijackNative *Hijack34mutex *sync.RWMutex35History []HistoryData36InteractshURLs []string37payloads map[string]interface{}38variables map[string]interface{}39lastActionNavigate *Action40}4142// HistoryData contains the page request/response pairs43type HistoryData struct {44RawRequest string45RawResponse string46}4748// Options contains additional configuration options for the browser instance49type Options struct {50Timeout time.Duration51DisableCookie bool52Options *types.Options53}5455// Run runs a list of actions by creating a new page in the browser.56func (i *Instance) Run(ctx *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (ActionData, *Page, error) {57page, err := i.engine.Page(proto.TargetCreateTarget{})58if err != nil {59return nil, nil, err60}61page = page.Timeout(options.Timeout)6263if err = i.browser.applyDefaultHeaders(page); err != nil {64_ = page.Close()65return nil, nil, err66}6768if i.browser.customAgent != "" {69if userAgentErr := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: i.browser.customAgent}); userAgentErr != nil {70_ = page.Close()71return nil, nil, userAgentErr72}73}7475payloads = generators.MergeMaps(payloads,76generators.BuildPayloadFromOptions(i.browser.options),77)7879target := ctx.MetaInput.Input80input, err := urlutil.Parse(target)81if err != nil {82_ = page.Close()83return nil, nil, errkit.Wrapf(err, "could not parse URL %s", target)84}8586hasTrailingSlash := httputil.HasTrailingSlash(target)87variables := utils.GenerateVariables(input, hasTrailingSlash, contextargs.GenerateVariables(ctx))88variables = generators.MergeMaps(variables, payloads)8990if vardump.EnableVarDump {91gologger.Debug().Msgf("Headless Protocol request variables: %s\n", vardump.DumpVariables(variables))92}9394createdPage := &Page{95options: options,96page: page,97ctx: ctx,98instance: i,99mutex: &sync.RWMutex{},100payloads: payloads,101variables: variables,102inputURL: input,103}104105successfulPageCreation := false106defer func() {107if !successfulPageCreation {108// to avoid leaking pages in case of errors109createdPage.Close()110}111}()112113httpclient, err := i.browser.getHTTPClient()114if err != nil {115return nil, nil, err116}117118// in case the page has request/response modification rules - enable global hijacking119if createdPage.hasModificationRules() || containsModificationActions(actions...) {120hijackRouter := page.HijackRequests()121if err := hijackRouter.Add("*", "", createdPage.routingRuleHandler(httpclient)); err != nil {122return nil, nil, err123}124createdPage.hijackRouter = hijackRouter125go hijackRouter.Run()126} else {127hijackRouter := NewHijack(page)128hijackRouter.SetPattern(&proto.FetchRequestPattern{129URLPattern: "*",130RequestStage: proto.FetchRequestStageResponse,131})132createdPage.hijackNative = hijackRouter133hijackRouterHandler := hijackRouter.Start(createdPage.routingRuleHandlerNative)134go func() {135_ = hijackRouterHandler()136}()137}138139if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{Viewport: &proto.PageViewport{140Scale: 1,141Width: float64(1920),142Height: float64(1080),143}}); err != nil {144return nil, nil, err145}146147// inject cookies148// each http request is performed via the native go http client149// we first inject the shared cookies150URL, err := url.Parse(ctx.MetaInput.Input)151if err != nil {152return nil, nil, err153}154155if !options.DisableCookie {156if cookies := ctx.CookieJar.Cookies(URL); len(cookies) > 0 {157var NetworkCookies []*proto.NetworkCookie158for _, cookie := range cookies {159networkCookie := &proto.NetworkCookie{160Name: cookie.Name,161Value: cookie.Value,162Domain: cookie.Domain,163Path: cookie.Path,164HTTPOnly: cookie.HttpOnly,165Secure: cookie.Secure,166Expires: proto.TimeSinceEpoch(cookie.Expires.Unix()),167SameSite: proto.NetworkCookieSameSite(GetSameSite(cookie)),168Priority: proto.NetworkCookiePriorityLow,169}170NetworkCookies = append(NetworkCookies, networkCookie)171}172params := proto.CookiesToParams(NetworkCookies)173for _, param := range params {174param.URL = ctx.MetaInput.Input175}176err := page.SetCookies(params)177if err != nil {178return nil, nil, err179}180}181}182183data, err := createdPage.ExecuteActions(ctx, actions)184if err != nil {185return nil, nil, err186}187188if !options.DisableCookie {189// at the end of actions pull out updated cookies from the browser and inject them into the shared cookie jar190if cookies, err := page.Cookies([]string{URL.String()}); !options.DisableCookie && err == nil && len(cookies) > 0 {191var httpCookies []*http.Cookie192for _, cookie := range cookies {193httpCookie := &http.Cookie{194Name: cookie.Name,195Value: cookie.Value,196Domain: cookie.Domain,197Path: cookie.Path,198HttpOnly: cookie.HTTPOnly,199Secure: cookie.Secure,200}201httpCookies = append(httpCookies, httpCookie)202}203ctx.CookieJar.SetCookies(URL, httpCookies)204}205}206207// The first item of history data will contain the very first request from the browser208// we assume it's the one matching the initial URL209createdPage.mutex.RLock()210var firstHistoryItem HistoryData211hasHistory := len(createdPage.History) > 0212if hasHistory {213firstHistoryItem = createdPage.History[0]214}215createdPage.mutex.RUnlock()216217if hasHistory {218if resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(firstHistoryItem.RawResponse)), nil); err == nil {219data["header"] = utils.HeadersToString(resp.Header)220data["status_code"] = fmt.Sprint(resp.StatusCode)221defer func() {222_ = resp.Body.Close()223}()224}225}226227successfulPageCreation = true // avoid closing the page in case of success in deferred function228return data, createdPage, nil229}230231// Close closes a browser page232func (p *Page) Close() {233if p.hijackRouter != nil {234_ = p.hijackRouter.Stop()235}236if p.hijackNative != nil {237_ = p.hijackNative.Stop()238}239_ = p.page.Close()240}241242// Page returns the current page for the actions243func (p *Page) Page() *rod.Page {244return p.page245}246247// Browser returns the browser that created the current page248func (p *Page) Browser() *rod.Browser {249return p.instance.engine250}251252// URL returns the URL for the current page.253func (p *Page) URL() string {254info, err := p.page.Info()255if err != nil {256return ""257}258return info.URL259}260261// DumpHistory returns the full page navigation history262func (p *Page) DumpHistory() string {263p.mutex.RLock()264defer p.mutex.RUnlock()265266var historyDump strings.Builder267for _, historyData := range p.History {268historyDump.WriteString(historyData.RawRequest)269historyDump.WriteString(historyData.RawResponse)270}271return historyDump.String()272}273274// addToHistory adds a request/response pair to the page history275func (p *Page) addToHistory(historyData ...HistoryData) {276p.mutex.Lock()277defer p.mutex.Unlock()278279p.History = append(p.History, historyData...)280}281282func (p *Page) addInteractshURL(URLs ...string) {283p.mutex.Lock()284defer p.mutex.Unlock()285286p.InteractshURLs = append(p.InteractshURLs, URLs...)287}288289func (p *Page) hasModificationRules() bool {290for _, rule := range p.rules {291if containsAnyModificationActionType(rule.Action) {292return true293}294}295return false296}297298// updateLastNavigatedURL updates the last navigated URL in the instance's299// request log.300func (p *Page) updateLastNavigatedURL() {301if p.lastActionNavigate == nil {302return303}304305templateURL := p.lastActionNavigate.GetArg("url")306p.instance.requestLog[templateURL] = p.URL()307}308309func containsModificationActions(actions ...*Action) bool {310for _, action := range actions {311if containsAnyModificationActionType(action.ActionType.ActionType) {312return true313}314}315return false316}317318func containsAnyModificationActionType(actionTypes ...ActionType) bool {319for _, actionType := range actionTypes {320switch actionType {321case ActionSetMethod:322return true323case ActionAddHeader:324return true325case ActionSetHeader:326return true327case ActionDeleteHeader:328return true329case ActionSetBody:330return true331}332}333return false334}335336func GetSameSite(cookie *http.Cookie) string {337switch cookie.SameSite {338case http.SameSiteNoneMode:339return "none"340case http.SameSiteLaxMode:341return "lax"342case http.SameSiteStrictMode:343return "strict"344case http.SameSiteDefaultMode:345fallthrough346default:347return ""348}349}350351352