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 }