Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ type AppConfig struct {
MaxProcesses int `mapstructure:"max_processes"`
IdleTTL time.Duration `mapstructure:"idle_ttl"`
MegaTimeout time.Duration `mapstructure:"mega_timeout"`
// BrowserControlURL optionally points to an existing CDP control URL (ws://...) instead of launching a local browser.
BrowserControlURL string `mapstructure:"browser_control_url"`
// DelegateStealth instructs the server to skip local stealth patching and
// rely on the remote browser (eg. Obscura) to provide stealth/covert features.
DelegateStealth bool `mapstructure:"delegate_stealth"`
}

type EngineConfig struct {
Expand Down Expand Up @@ -178,19 +183,21 @@ func sanitizedConfigForLog(cfg Config) map[string]interface{} {
return map[string]interface{}{
"server": cfg.Server,
"app": map[string]interface{}{
"timeout": cfg.App.Timeout,
"browser_path": cfg.App.BrowserPath != "",
"profiles": cfg.App.ProfilesJSON != "",
"head": cfg.App.IsBrowserHead,
"leave_head": cfg.App.IsLeaveHead,
"leakless": cfg.App.IsLeakless,
"block_resources": cfg.App.BlockResources,
"block_trackers": cfg.App.BlockTrackers,
"debug_endpoints": cfg.App.DebugEndpoints,
"log_format": cfg.App.LogFormat,
"max_processes": cfg.App.MaxProcesses,
"idle_ttl": cfg.App.IdleTTL.String(),
"mega_timeout": cfg.App.MegaTimeout.String(),
"timeout": cfg.App.Timeout,
"browser_path": cfg.App.BrowserPath != "",
"profiles": cfg.App.ProfilesJSON != "",
"head": cfg.App.IsBrowserHead,
"leave_head": cfg.App.IsLeaveHead,
"leakless": cfg.App.IsLeakless,
"block_resources": cfg.App.BlockResources,
"block_trackers": cfg.App.BlockTrackers,
"debug_endpoints": cfg.App.DebugEndpoints,
"log_format": cfg.App.LogFormat,
"max_processes": cfg.App.MaxProcesses,
"idle_ttl": cfg.App.IdleTTL.String(),
"mega_timeout": cfg.App.MegaTimeout.String(),
"browser_control": cfg.App.BrowserControlURL != "",
"delegate_stealth": cfg.App.DelegateStealth,
},
"proxies": map[string]interface{}{
"global": maskedProxyForLog(cfg.Proxies.Global),
Expand Down Expand Up @@ -411,6 +418,8 @@ func setConfigDefaults(v *viper.Viper) {
v.SetDefault("app.max_processes", 4)
v.SetDefault("app.idle_ttl", "10m")
v.SetDefault("app.mega_timeout", "90s")
v.SetDefault("app.browser_control_url", "")
v.SetDefault("app.delegate_stealth", false)

v.SetDefault("proxies.entries", []interface{}{})
v.SetDefault("proxies.global", "")
Expand Down Expand Up @@ -449,6 +458,8 @@ func init() {
RootCmd.PersistentFlags().StringVarP(&config.Server.ConfigPath, "config", "c", "", "Configuration file path")
RootCmd.PersistentFlags().StringVarP(&config.App.BrowserPath, "browser-path", "", "", "Custom browser binary path (Chrome/Chromium/Edge/Brave..)")
RootCmd.PersistentFlags().StringVar(&config.App.ProfilesJSON, "profiles", "", "Path to browser profile catalog JSON")
RootCmd.PersistentFlags().StringVar(&config.App.BrowserControlURL, "browser-control-url", "", "Remote CDP control URL (ws://...) to use instead of launching local browser")
RootCmd.PersistentFlags().BoolVar(&config.App.DelegateStealth, "delegate-stealth", false, "Delegate stealth/anti-detection handling to the remote browser (eg. Obscura)")
RootCmd.PersistentFlags().BoolVarP(&config.Server.IsVerbose, "verbose", "v", false, "Use verbose output")
RootCmd.PersistentFlags().BoolVarP(&config.Server.IsDebug, "debug", "d", false, "Use debug output. Disable headless browser")
RootCmd.PersistentFlags().BoolVarP(&config.Server.IsQuiet, "quiet", "q", false, "Suppress info logs on stderr (default for CLI commands)")
Expand Down
16 changes: 9 additions & 7 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,15 @@ func buildFingerprintBrowserOptions() core.BrowserOpts {
blockedResourceTypes := core.MustParseBlockedResourceTypes(config.App.BlockResources)

opts := core.BrowserOpts{
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
BrowserPath: config.App.BrowserPath,
Insecure: config.Server.Insecure,
BlockResourceTypes: blockedResourceTypes,
BlockTrackers: config.App.BlockTrackers,
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
BrowserPath: config.App.BrowserPath,
BrowserControlURL: config.App.BrowserControlURL,
DelegateStealthToRemote: config.App.DelegateStealth,
Insecure: config.Server.Insecure,
BlockResourceTypes: blockedResourceTypes,
BlockTrackers: config.App.BlockTrackers,
}
if config.Server.IsDebug {
opts.IsHeadless = false
Expand Down
31 changes: 29 additions & 2 deletions core/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type BrowserOpts struct {
Insecure bool
// UserAgent optionally overrides browser-reported user agent during emulation.
UserAgent string
// BrowserControlURL optionally points to an existing CDP control URL (ws://...) instead of launching a local browser.
BrowserControlURL string
// DelegateStealthToRemote, when true, skips running the local stealth/patch JS
// because the remote browser (eg. Obscura) will handle stealth/covert behaviour.
DelegateStealthToRemote bool
// BlockResourceTypes are blocked during page navigation when non-empty.
// Typical tokens map to these types: image, font, css(stylesheet), js(script), media.
BlockResourceTypes []proto.NetworkResourceType
Expand Down Expand Up @@ -274,6 +279,23 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) {
}
logrus.WithFields(browserOptsLogFields(opts)).Debug("Browser options")

// If a remote CDP control URL is provided (e.g. Obscura), use it instead of
// launching a local browser. This makes Obscura a drop-in replacement for
// Chrome for the CDP-based integration.
if strings.TrimSpace(opts.BrowserControlURL) != "" {
b := Browser{
conn: &browserConnection{},
}
b.BrowserOpts = opts
b.browserAddr = strings.TrimSpace(opts.BrowserControlURL)

if opts.CaptchaSolverEnabled && opts.CaptchaSolverApiKey != "" {
b.CaptchaSolver = NewSolver(opts.CaptchaSolverApiKey)
logrus.Debug("Captcha solver initialized")
}
return &b, nil
}

path, err := resolveBrowserBinaryPath(opts.BrowserPath, launcher.LookPath)
if err != nil {
return nil, err
Expand Down Expand Up @@ -362,6 +384,8 @@ func browserOptsLogFields(opts BrowserOpts) logrus.Fields {
"block_resource_types": len(opts.BlockResourceTypes),
"block_trackers": opts.BlockTrackers,
"proxy_lanes_enabled": opts.ProxyLaneStore != nil,
"browser_control_url": strings.TrimSpace(opts.BrowserControlURL) != "",
"delegate_stealth": opts.DelegateStealthToRemote,
}
}

Expand Down Expand Up @@ -1460,11 +1484,14 @@ func (b *Browser) Navigate(ctx context.Context, URL string) (*rod.Page, error) {
profile, laneKey := b.laneProfile(ctx, browser)
SetBrowserProfileID(ctx, profile.ID)
minimalProfile := minimalBrowserProfileFromContext(ctx)
// If stealth is delegated to the remote browser (eg. Obscura), skip local
// worker/script stealth patching while still applying UA/locale/headers.
effectiveMinimal := minimalProfile || b.DelegateStealthToRemote
WithRequest(ctx).WithFields(logrus.Fields{
"lane_id": laneKey,
"minimal_profile": minimalProfile,
}).Info("Browser profile selected")
if err := applyProfile(page, profile, minimalProfile); err != nil {
if err := applyProfile(page, profile, effectiveMinimal); err != nil {
closeOnErr()
return nil, fmt.Errorf("apply profile %s (%s) failed: %w", profile.ID, laneKey, err)
}
Expand All @@ -1474,7 +1501,7 @@ func (b *Browser) Navigate(ctx context.Context, URL string) (*rod.Page, error) {
}

page = page.Context(ctx)
if !minimalProfile {
if !effectiveMinimal {
metrics := profileDisplayMetricsFor(profile)
patchScript, err := buildProfilePatchScript(profile, profileNavigatorLanguages(profile), metrics)
if err != nil {
Expand Down
Loading