Path: blob/dev/pkg/protocols/headless/engine/page.go
2072 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 {64return nil, nil, err65}6667if i.browser.customAgent != "" {68if userAgentErr := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: i.browser.customAgent}); userAgentErr != nil {69return nil, nil, userAgentErr70}71}7273payloads = generators.MergeMaps(payloads,74generators.BuildPayloadFromOptions(i.browser.options),75)7677target := ctx.MetaInput.Input78input, err := urlutil.Parse(target)79if err != nil {80return nil, nil, errkit.Wrapf(err, "could not parse URL %s", target)81}8283hasTrailingSlash := httputil.HasTrailingSlash(target)84variables := utils.GenerateVariables(input, hasTrailingSlash, contextargs.GenerateVariables(ctx))85variables = generators.MergeMaps(variables, payloads)8687if vardump.EnableVarDump {88gologger.Debug().Msgf("Headless Protocol request variables: %s\n", vardump.DumpVariables(variables))89}9091createdPage := &Page{92options: options,93page: page,94ctx: ctx,95instance: i,96mutex: &sync.RWMutex{},97payloads: payloads,98variables: variables,99inputURL: input,100}101102httpclient, err := i.browser.getHTTPClient()103if err != nil {104return nil, nil, err105}106107// in case the page has request/response modification rules - enable global hijacking108if createdPage.hasModificationRules() || containsModificationActions(actions...) {109hijackRouter := page.HijackRequests()110if err := hijackRouter.Add("*", "", createdPage.routingRuleHandler(httpclient)); err != nil {111return nil, nil, err112}113createdPage.hijackRouter = hijackRouter114go hijackRouter.Run()115} else {116hijackRouter := NewHijack(page)117hijackRouter.SetPattern(&proto.FetchRequestPattern{118URLPattern: "*",119RequestStage: proto.FetchRequestStageResponse,120})121createdPage.hijackNative = hijackRouter122hijackRouterHandler := hijackRouter.Start(createdPage.routingRuleHandlerNative)123go func() {124_ = hijackRouterHandler()125}()126}127128if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{Viewport: &proto.PageViewport{129Scale: 1,130Width: float64(1920),131Height: float64(1080),132}}); err != nil {133return nil, nil, err134}135136// inject cookies137// each http request is performed via the native go http client138// we first inject the shared cookies139URL, err := url.Parse(ctx.MetaInput.Input)140if err != nil {141return nil, nil, err142}143144if !options.DisableCookie {145if cookies := ctx.CookieJar.Cookies(URL); len(cookies) > 0 {146var NetworkCookies []*proto.NetworkCookie147for _, cookie := range cookies {148networkCookie := &proto.NetworkCookie{149Name: cookie.Name,150Value: cookie.Value,151Domain: cookie.Domain,152Path: cookie.Path,153HTTPOnly: cookie.HttpOnly,154Secure: cookie.Secure,155Expires: proto.TimeSinceEpoch(cookie.Expires.Unix()),156SameSite: proto.NetworkCookieSameSite(GetSameSite(cookie)),157Priority: proto.NetworkCookiePriorityLow,158}159NetworkCookies = append(NetworkCookies, networkCookie)160}161params := proto.CookiesToParams(NetworkCookies)162for _, param := range params {163param.URL = ctx.MetaInput.Input164}165err := page.SetCookies(params)166if err != nil {167return nil, nil, err168}169}170}171172data, err := createdPage.ExecuteActions(ctx, actions)173if err != nil {174return nil, nil, err175}176177if !options.DisableCookie {178// at the end of actions pull out updated cookies from the browser and inject them into the shared cookie jar179if cookies, err := page.Cookies([]string{URL.String()}); !options.DisableCookie && err == nil && len(cookies) > 0 {180var httpCookies []*http.Cookie181for _, cookie := range cookies {182httpCookie := &http.Cookie{183Name: cookie.Name,184Value: cookie.Value,185Domain: cookie.Domain,186Path: cookie.Path,187HttpOnly: cookie.HTTPOnly,188Secure: cookie.Secure,189}190httpCookies = append(httpCookies, httpCookie)191}192ctx.CookieJar.SetCookies(URL, httpCookies)193}194}195196// The first item of history data will contain the very first request from the browser197// we assume it's the one matching the initial URL198if len(createdPage.History) > 0 {199firstItem := createdPage.History[0]200if resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(firstItem.RawResponse)), nil); err == nil {201data["header"] = utils.HeadersToString(resp.Header)202data["status_code"] = fmt.Sprint(resp.StatusCode)203defer func() {204_ = resp.Body.Close()205}()206}207}208209return data, createdPage, nil210}211212// Close closes a browser page213func (p *Page) Close() {214if p.hijackRouter != nil {215_ = p.hijackRouter.Stop()216}217if p.hijackNative != nil {218_ = p.hijackNative.Stop()219}220_ = p.page.Close()221}222223// Page returns the current page for the actions224func (p *Page) Page() *rod.Page {225return p.page226}227228// Browser returns the browser that created the current page229func (p *Page) Browser() *rod.Browser {230return p.instance.engine231}232233// URL returns the URL for the current page.234func (p *Page) URL() string {235info, err := p.page.Info()236if err != nil {237return ""238}239return info.URL240}241242// DumpHistory returns the full page navigation history243func (p *Page) DumpHistory() string {244p.mutex.RLock()245defer p.mutex.RUnlock()246247var historyDump strings.Builder248for _, historyData := range p.History {249historyDump.WriteString(historyData.RawRequest)250historyDump.WriteString(historyData.RawResponse)251}252return historyDump.String()253}254255// addToHistory adds a request/response pair to the page history256func (p *Page) addToHistory(historyData ...HistoryData) {257p.mutex.Lock()258defer p.mutex.Unlock()259260p.History = append(p.History, historyData...)261}262263func (p *Page) addInteractshURL(URLs ...string) {264p.mutex.Lock()265defer p.mutex.Unlock()266267p.InteractshURLs = append(p.InteractshURLs, URLs...)268}269270func (p *Page) hasModificationRules() bool {271for _, rule := range p.rules {272if containsAnyModificationActionType(rule.Action) {273return true274}275}276return false277}278279// updateLastNavigatedURL updates the last navigated URL in the instance's280// request log.281func (p *Page) updateLastNavigatedURL() {282if p.lastActionNavigate == nil {283return284}285286templateURL := p.lastActionNavigate.GetArg("url")287p.instance.requestLog[templateURL] = p.URL()288}289290func containsModificationActions(actions ...*Action) bool {291for _, action := range actions {292if containsAnyModificationActionType(action.ActionType.ActionType) {293return true294}295}296return false297}298299func containsAnyModificationActionType(actionTypes ...ActionType) bool {300for _, actionType := range actionTypes {301switch actionType {302case ActionSetMethod:303return true304case ActionAddHeader:305return true306case ActionSetHeader:307return true308case ActionDeleteHeader:309return true310case ActionSetBody:311return true312}313}314return false315}316317func GetSameSite(cookie *http.Cookie) string {318switch cookie.SameSite {319case http.SameSiteNoneMode:320return "none"321case http.SameSiteLaxMode:322return "lax"323case http.SameSiteStrictMode:324return "strict"325case http.SameSiteDefaultMode:326fallthrough327default:328return ""329}330}331332333