github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/headless/engine/page.go (about)

     1  package engine
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/go-rod/rod"
    13  	"github.com/go-rod/rod/lib/proto"
    14  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
    15  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
    16  	"github.com/projectdiscovery/nuclei/v2/pkg/types"
    17  )
    18  
    19  // Page is a single page in an isolated browser instance
    20  type Page struct {
    21  	input          *contextargs.Context
    22  	options        *Options
    23  	page           *rod.Page
    24  	rules          []rule
    25  	instance       *Instance
    26  	hijackRouter   *rod.HijackRouter
    27  	hijackNative   *Hijack
    28  	mutex          *sync.RWMutex
    29  	History        []HistoryData
    30  	InteractshURLs []string
    31  	payloads       map[string]interface{}
    32  }
    33  
    34  // HistoryData contains the page request/response pairs
    35  type HistoryData struct {
    36  	RawRequest  string
    37  	RawResponse string
    38  }
    39  
    40  // Options contains additional configuration options for the browser instance
    41  type Options struct {
    42  	Timeout     time.Duration
    43  	CookieReuse bool
    44  	Options     *types.Options
    45  }
    46  
    47  // Run runs a list of actions by creating a new page in the browser.
    48  func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (map[string]string, *Page, error) {
    49  	page, err := i.engine.Page(proto.TargetCreateTarget{})
    50  	if err != nil {
    51  		return nil, nil, err
    52  	}
    53  	page = page.Timeout(options.Timeout)
    54  
    55  	if i.browser.customAgent != "" {
    56  		if userAgentErr := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: i.browser.customAgent}); userAgentErr != nil {
    57  			return nil, nil, userAgentErr
    58  		}
    59  	}
    60  
    61  	createdPage := &Page{
    62  		options:  options,
    63  		page:     page,
    64  		input:    input,
    65  		instance: i,
    66  		mutex:    &sync.RWMutex{},
    67  		payloads: payloads,
    68  	}
    69  
    70  	// in case the page has request/response modification rules - enable global hijacking
    71  	if createdPage.hasModificationRules() || containsModificationActions(actions...) {
    72  		hijackRouter := page.HijackRequests()
    73  		if err := hijackRouter.Add("*", "", createdPage.routingRuleHandler); err != nil {
    74  			return nil, nil, err
    75  		}
    76  		createdPage.hijackRouter = hijackRouter
    77  		go hijackRouter.Run()
    78  	} else {
    79  		hijackRouter := NewHijack(page)
    80  		hijackRouter.SetPattern(&proto.FetchRequestPattern{
    81  			URLPattern:   "*",
    82  			RequestStage: proto.FetchRequestStageResponse,
    83  		})
    84  		createdPage.hijackNative = hijackRouter
    85  		hijackRouterHandler := hijackRouter.Start(createdPage.routingRuleHandlerNative)
    86  		go func() {
    87  			_ = hijackRouterHandler()
    88  		}()
    89  	}
    90  
    91  	if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{Viewport: &proto.PageViewport{
    92  		Scale:  1,
    93  		Width:  float64(1920),
    94  		Height: float64(1080),
    95  	}}); err != nil {
    96  		return nil, nil, err
    97  	}
    98  
    99  	if _, err := page.SetExtraHeaders([]string{"Accept-Language", "en, en-GB, en-us;"}); err != nil {
   100  		return nil, nil, err
   101  	}
   102  
   103  	// inject cookies
   104  	// each http request is performed via the native go http client
   105  	// we first inject the shared cookies
   106  	URL, err := url.Parse(input.MetaInput.Input)
   107  	if err != nil {
   108  		return nil, nil, err
   109  	}
   110  
   111  	if options.CookieReuse {
   112  		if cookies := input.CookieJar.Cookies(URL); len(cookies) > 0 {
   113  			var NetworkCookies []*proto.NetworkCookie
   114  			for _, cookie := range cookies {
   115  				networkCookie := &proto.NetworkCookie{
   116  					Name:     cookie.Name,
   117  					Value:    cookie.Value,
   118  					Domain:   cookie.Domain,
   119  					Path:     cookie.Path,
   120  					HTTPOnly: cookie.HttpOnly,
   121  					Secure:   cookie.Secure,
   122  					Expires:  proto.TimeSinceEpoch(cookie.Expires.Unix()),
   123  					SameSite: proto.NetworkCookieSameSite(GetSameSite(cookie)),
   124  					Priority: proto.NetworkCookiePriorityLow,
   125  				}
   126  				NetworkCookies = append(NetworkCookies, networkCookie)
   127  			}
   128  			params := proto.CookiesToParams(NetworkCookies)
   129  			for _, param := range params {
   130  				param.URL = input.MetaInput.Input
   131  			}
   132  			err := page.SetCookies(params)
   133  			if err != nil {
   134  				return nil, nil, err
   135  			}
   136  		}
   137  	}
   138  
   139  	data, err := createdPage.ExecuteActions(input, actions, payloads)
   140  	if err != nil {
   141  		return nil, nil, err
   142  	}
   143  
   144  	if options.CookieReuse {
   145  		// at the end of actions pull out updated cookies from the browser and inject them into the shared cookie jar
   146  		if cookies, err := page.Cookies([]string{URL.String()}); options.CookieReuse && err == nil && len(cookies) > 0 {
   147  			var httpCookies []*http.Cookie
   148  			for _, cookie := range cookies {
   149  				httpCookie := &http.Cookie{
   150  					Name:     cookie.Name,
   151  					Value:    cookie.Value,
   152  					Domain:   cookie.Domain,
   153  					Path:     cookie.Path,
   154  					HttpOnly: cookie.HTTPOnly,
   155  					Secure:   cookie.Secure,
   156  				}
   157  				httpCookies = append(httpCookies, httpCookie)
   158  			}
   159  			input.CookieJar.SetCookies(URL, httpCookies)
   160  		}
   161  	}
   162  
   163  	// The first item of history data will contain the very first request from the browser
   164  	// we assume it's the one matching the initial URL
   165  	if len(createdPage.History) > 0 {
   166  		firstItem := createdPage.History[0]
   167  		if resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(firstItem.RawResponse)), nil); err == nil {
   168  			data["header"] = utils.HeadersToString(resp.Header)
   169  			data["status_code"] = fmt.Sprint(resp.StatusCode)
   170  			resp.Body.Close()
   171  		}
   172  	}
   173  
   174  	return data, createdPage, nil
   175  }
   176  
   177  // Close closes a browser page
   178  func (p *Page) Close() {
   179  	if p.hijackRouter != nil {
   180  		_ = p.hijackRouter.Stop()
   181  	}
   182  	if p.hijackNative != nil {
   183  		_ = p.hijackNative.Stop()
   184  	}
   185  	p.page.Close()
   186  }
   187  
   188  // Page returns the current page for the actions
   189  func (p *Page) Page() *rod.Page {
   190  	return p.page
   191  }
   192  
   193  // Browser returns the browser that created the current page
   194  func (p *Page) Browser() *rod.Browser {
   195  	return p.instance.engine
   196  }
   197  
   198  // URL returns the URL for the current page.
   199  func (p *Page) URL() string {
   200  	info, err := p.page.Info()
   201  	if err != nil {
   202  		return ""
   203  	}
   204  	return info.URL
   205  }
   206  
   207  // DumpHistory returns the full page navigation history
   208  func (p *Page) DumpHistory() string {
   209  	p.mutex.RLock()
   210  	defer p.mutex.RUnlock()
   211  
   212  	var historyDump strings.Builder
   213  	for _, historyData := range p.History {
   214  		historyDump.WriteString(historyData.RawRequest)
   215  		historyDump.WriteString(historyData.RawResponse)
   216  	}
   217  	return historyDump.String()
   218  }
   219  
   220  // addToHistory adds a request/response pair to the page history
   221  func (p *Page) addToHistory(historyData ...HistoryData) {
   222  	p.mutex.Lock()
   223  	defer p.mutex.Unlock()
   224  
   225  	p.History = append(p.History, historyData...)
   226  }
   227  
   228  func (p *Page) addInteractshURL(URLs ...string) {
   229  	p.mutex.Lock()
   230  	defer p.mutex.Unlock()
   231  
   232  	p.InteractshURLs = append(p.InteractshURLs, URLs...)
   233  }
   234  
   235  func (p *Page) hasModificationRules() bool {
   236  	for _, rule := range p.rules {
   237  		if containsAnyModificationActionType(rule.Action) {
   238  			return true
   239  		}
   240  	}
   241  	return false
   242  }
   243  
   244  func containsModificationActions(actions ...*Action) bool {
   245  	for _, action := range actions {
   246  		if containsAnyModificationActionType(action.ActionType.ActionType) {
   247  			return true
   248  		}
   249  	}
   250  	return false
   251  }
   252  
   253  func containsAnyModificationActionType(actionTypes ...ActionType) bool {
   254  	for _, actionType := range actionTypes {
   255  		switch actionType {
   256  		case ActionSetMethod:
   257  			return true
   258  		case ActionAddHeader:
   259  			return true
   260  		case ActionSetHeader:
   261  			return true
   262  		case ActionDeleteHeader:
   263  			return true
   264  		case ActionSetBody:
   265  			return true
   266  		}
   267  	}
   268  	return false
   269  }
   270  
   271  func GetSameSite(cookie *http.Cookie) string {
   272  	switch cookie.SameSite {
   273  	case http.SameSiteNoneMode:
   274  		return "none"
   275  	case http.SameSiteLaxMode:
   276  		return "lax"
   277  	case http.SameSiteStrictMode:
   278  		return "strict"
   279  	case http.SameSiteDefaultMode:
   280  		fallthrough
   281  	default:
   282  		return ""
   283  	}
   284  }