From a4d459761e21e207bebb724baec27116133cfe60 Mon Sep 17 00:00:00 2001 From: Rustem Kamalov Date: Tue, 17 Mar 2026 02:18:51 +0300 Subject: [PATCH] Add custom browser path support --- cmd/root.go | 5 ++++- cmd/search.go | 1 + cmd/serve.go | 1 + config.yaml | 3 +++ core/browser.go | 44 ++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 509ce85..0f589ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,7 +13,7 @@ import ( ) const ( - version = "0.5.5" + version = "0.5.6" defaultConfigFilename = "config" envPrefix = "OPENSERP" ) @@ -37,6 +37,7 @@ type AppConfig struct { Port int `mapstructure:"port"` Timeout int `mapstructure:"timeout"` ConfigPath string `mapstructure:"config_path"` + BrowserPath string `mapstructure:"browser_path"` IsBrowserHead bool `mapstructure:"head"` IsLeaveHead bool `mapstructure:"leave_head"` IsLeakless bool `mapstructure:"leakless"` @@ -52,6 +53,7 @@ var config = Config{} var flagToConfigKey = map[string]string{ "config": "app.config_path", + "browser-path": "app.browser_path", "leave": "app.leave_head", "raw": "app.raw_requests", "2captcha_key": "2captcha.apikey", @@ -163,6 +165,7 @@ func init() { RootCmd.PersistentFlags().StringVarP(&config.App.Host, "host", "a", "127.0.0.1", "Host address to run server") RootCmd.PersistentFlags().IntVarP(&config.App.Timeout, "timeout", "t", 30, "Timeout to fail request") RootCmd.PersistentFlags().StringVarP(&config.App.ConfigPath, "config", "c", "", "Configuration file path") + RootCmd.PersistentFlags().StringVarP(&config.App.BrowserPath, "browser-path", "", "", "Custom browser binary path (Chrome/Chromium/Edge/Brave..)") RootCmd.PersistentFlags().BoolVarP(&config.App.IsVerbose, "verbose", "v", false, "Use verbose output") RootCmd.PersistentFlags().BoolVarP(&config.App.IsDebug, "debug", "d", false, "Use debug output. Disable headless browser") RootCmd.PersistentFlags().BoolVarP(&config.App.IsBrowserHead, "head", "", false, "Enable browser UI") diff --git a/cmd/search.go b/cmd/search.go index a32720c..cf25624 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -70,6 +70,7 @@ func searchBrowser(engineType string, query core.Query) ([]core.SearchResult, er Timeout: time.Second * time.Duration(config.App.Timeout), LeavePageOpen: config.App.IsLeaveHead, CaptchaSolverApiKey: config.Config2Capcha.ApiKey, + BrowserPath: config.App.BrowserPath, ProxyURL: config.App.ProxyURL, Insecure: config.App.Insecure, UseStealth: config.App.IsStealth, diff --git a/cmd/serve.go b/cmd/serve.go index c10b8aa..30251d1 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -80,6 +80,7 @@ func serve(cmd *cobra.Command, args []string) { Timeout: time.Second * time.Duration(config.App.Timeout), LeavePageOpen: config.App.IsLeaveHead, CaptchaSolverApiKey: config.Config2Capcha.ApiKey, + BrowserPath: config.App.BrowserPath, ProxyURL: config.App.ProxyURL, Insecure: config.App.Insecure, UseStealth: config.App.IsStealth, diff --git a/config.yaml b/config.yaml index 0ec5787..a0596b5 100644 --- a/config.yaml +++ b/config.yaml @@ -10,6 +10,9 @@ app: stealth: false insecure: true + # Optional custom browser binary path (chrome/chromium/edge..) + #browser_path: "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe" + 2captcha: apikey: "123123123123123" diff --git a/core/browser.go b/core/browser.go index 650dc2d..2aab456 100644 --- a/core/browser.go +++ b/core/browser.go @@ -3,6 +3,7 @@ package core import ( "fmt" "net/url" + "os" "strings" "time" @@ -23,6 +24,7 @@ type BrowserOpts struct { LeavePageOpen bool // Leave pages and browser open WaitLoadTime time.Duration // Time to wait till page loads CaptchaSolverApiKey string // 2Captcha api key + BrowserPath string // Explicit browser executable path ProxyURL string // Proxy URL Insecure bool // Allow insecure TLS connections UseStealth bool // Use go-rod stealth plugin @@ -51,12 +53,18 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) { opts.Check() logrus.Debugf("Browser options: %+v", opts) - path, has := launcher.LookPath() - logrus.Debug("Browser found: ", has) + path, err := resolveBrowserBinaryPath(opts.BrowserPath, launcher.LookPath) + if err != nil { + return nil, err + } // Create launcher - l := launcher.New().Bin(path).Leakless(opts.IsLeakless).Headless(opts.IsHeadless).Set("disable-blink-features", "AutomationControlled"). + l := launcher.New().Leakless(opts.IsLeakless).Headless(opts.IsHeadless).Set("disable-blink-features", "AutomationControlled"). Delete("enable-automation") + if path != "" { + logrus.Debugf("Using browser binary: %s", path) + l = l.Bin(path) + } // Configure proxy if specified if opts.ProxyURL != "" { @@ -79,7 +87,6 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) { } } - var err error b := Browser{BrowserOpts: opts} b.browserAddr, err = l.Launch() @@ -91,6 +98,35 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) { return &b, err } +func validateBrowserBinaryPath(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("path points to a directory") + } + return nil +} + +// resolveBrowserBinaryPath prefers an explicit browser path. If no explicit path is provided, +// it falls back to launcher autodiscovery and lets Rod handle auto-download when no binary is found. +func resolveBrowserBinaryPath(browserPath string, lookPath func() (string, bool)) (string, error) { + if browserPath != "" { + if err := validateBrowserBinaryPath(browserPath); err != nil { + return "", fmt.Errorf("invalid browser_path %q: %w", browserPath, err) + } + return browserPath, nil + } + + path, has := lookPath() + if has { + return path, nil + } + + return "", nil +} + // Check whether browser instance is already created func (b *Browser) IsInitialized() bool { if b.browserAddr != "" {