Chris Grieger bc551fc3f1 fix: improve headless engine startup and shutdown
Fixes #6221

Instead of enumerating all chrome processes to determine
which ones need to be killed on shutdown, use the launcher.Kill()
method to terminate the process that was launched for this
browser instance.
2025-05-14 16:14:21 +02:00

147 lines
3.8 KiB
Go

package engine
import (
"fmt"
"net/http"
"os"
"strings"
"sync"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/launcher/flags"
"github.com/pkg/errors"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
fileutil "github.com/projectdiscovery/utils/file"
osutils "github.com/projectdiscovery/utils/os"
)
// Browser is a browser structure for nuclei headless module
type Browser struct {
customAgent string
tempDir string
engine *rod.Browser
options *types.Options
launcher *launcher.Launcher
// use getHTTPClient to get the http client
httpClient *http.Client
httpClientOnce *sync.Once
}
// New creates a new nuclei headless browser module
func New(options *types.Options) (*Browser, error) {
dataStore, err := os.MkdirTemp("", "nuclei-*")
if err != nil {
return nil, errors.Wrap(err, "could not create temporary directory")
}
chromeLauncher := launcher.New().
Leakless(false).
Set("disable-gpu", "true").
Set("ignore-certificate-errors", "true").
Set("ignore-certificate-errors", "1").
Set("disable-crash-reporter", "true").
Set("disable-notifications", "true").
Set("hide-scrollbars", "true").
Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)).
Set("mute-audio", "true").
Set("incognito", "true").
Delete("use-mock-keychain").
UserDataDir(dataStore)
if MustDisableSandbox() {
chromeLauncher = chromeLauncher.NoSandbox(true)
}
executablePath, err := os.Executable()
if err != nil {
return nil, err
}
// if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome
useMusl, _ := fileutil.UseMusl(executablePath)
if options.UseInstalledChrome || useMusl {
if chromePath, hasChrome := launcher.LookPath(); hasChrome {
chromeLauncher.Bin(chromePath)
} else {
return nil, errors.New("the chrome browser is not installed")
}
}
if options.ShowBrowser {
chromeLauncher = chromeLauncher.Headless(false)
} else {
chromeLauncher = chromeLauncher.Headless(true)
}
if options.AliveHttpProxy != "" {
chromeLauncher = chromeLauncher.Proxy(options.AliveHttpProxy)
}
for k, v := range options.ParseHeadlessOptionalArguments() {
chromeLauncher.Set(flags.Flag(k), v)
}
launcherURL, err := chromeLauncher.Launch()
if err != nil {
return nil, err
}
browser := rod.New().ControlURL(launcherURL)
if browserErr := browser.Connect(); browserErr != nil {
return nil, browserErr
}
customAgent := ""
for _, option := range options.CustomHeaders {
parts := strings.SplitN(option, ":", 2)
if len(parts) != 2 {
continue
}
if strings.EqualFold(parts[0], "User-Agent") {
customAgent = parts[1]
}
}
engine := &Browser{
tempDir: dataStore,
customAgent: customAgent,
engine: browser,
options: options,
httpClientOnce: &sync.Once{},
launcher: chromeLauncher,
}
return engine, nil
}
// MustDisableSandbox determines if the current os and user needs sandbox mode disabled
func MustDisableSandbox() bool {
// linux with root user needs "--no-sandbox" option
// https://github.com/chromium/chromium/blob/c4d3c31083a2e1481253ff2d24298a1dfe19c754/chrome/test/chromedriver/client/chromedriver.py#L209
return osutils.IsLinux()
}
// SetUserAgent sets custom user agent to the browser
func (b *Browser) SetUserAgent(customUserAgent string) {
b.customAgent = customUserAgent
}
// UserAgent fetch the currently set custom user agent
func (b *Browser) UserAgent() string {
return b.customAgent
}
func (b *Browser) getHTTPClient() (*http.Client, error) {
var err error
b.httpClientOnce.Do(func() {
b.httpClient, err = newHttpClient(b.options)
})
return b.httpClient, err
}
// Close closes the browser engine
func (b *Browser) Close() {
b.engine.Close()
b.launcher.Kill()
os.RemoveAll(b.tempDir)
}