2023-06-23 04:08:00 +03:00
package cmd
import (
"fmt"
2026-04-04 19:32:23 +03:00
"os"
2025-09-30 05:27:54 +03:00
"strconv"
2023-06-30 22:49:58 +03:00
"strings"
2023-06-23 04:08:00 +03:00
2023-06-23 04:16:45 +03:00
"github.com/karust/openserp/core"
2023-06-23 04:08:00 +03:00
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const (
2026-04-20 20:12:19 +03:00
version = "0.6.5"
2023-06-30 22:49:58 +03:00
defaultConfigFilename = "config"
envPrefix = "OPENSERP"
2023-06-23 04:08:00 +03:00
)
2023-06-30 22:49:58 +03:00
type Config struct {
2026-04-01 00:54:21 +03:00
Server ServerConfig ` mapstructure:"server" `
App AppConfig ` mapstructure:"app" `
Proxies core . ProxiesConfig ` mapstructure:"proxies" `
Cache CacheConfig ` mapstructure:"cache" `
Resilience ResilienceConfig ` mapstructure:"resilience" `
CircuitBreaker CircuitBreakerConfig ` mapstructure:"circuit_breaker" `
CORS CORSConfig ` mapstructure:"cors" `
2026-04-20 20:12:19 +03:00
Captcha CaptchaConfig ` mapstructure:"captcha" `
2026-04-01 00:54:21 +03:00
Config2Capcha Config2Captcha ` mapstructure:"2captcha" `
GoogleConfig EngineConfig ` mapstructure:"google" `
YandexConfig EngineConfig ` mapstructure:"yandex" `
BaiduConfig EngineConfig ` mapstructure:"baidu" `
BingConfig EngineConfig ` mapstructure:"bing" `
DuckDuckGoConfig EngineConfig ` mapstructure:"duckduckgo" `
2024-05-06 01:13:07 +03:00
}
type Config2Captcha struct {
ApiKey string ` mapstructure:"apikey" `
2023-06-30 22:49:58 +03:00
}
2026-04-01 00:54:21 +03:00
type ServerConfig struct {
2023-06-30 22:49:58 +03:00
Host string ` mapstructure:"host" `
Port int ` mapstructure:"port" `
ConfigPath string ` mapstructure:"config_path" `
IsDebug bool ` mapstructure:"debug" `
IsVerbose bool ` mapstructure:"verbose" `
IsRawRequests bool ` mapstructure:"raw_requests" `
2025-04-08 16:06:41 +08:00
Insecure bool ` mapstructure:"insecure" `
2026-04-01 00:54:21 +03:00
}
type AppConfig struct {
Timeout int ` mapstructure:"timeout" `
BrowserPath string ` mapstructure:"browser_path" `
IsBrowserHead bool ` mapstructure:"head" `
IsLeaveHead bool ` mapstructure:"leave_head" `
IsLeakless bool ` mapstructure:"leakless" `
2025-08-18 22:49:45 +03:00
IsStealth bool ` mapstructure:"stealth" `
2023-06-23 04:08:00 +03:00
}
2026-04-01 00:54:21 +03:00
type EngineConfig struct {
core . SearchEngineOptions ` mapstructure:",squash" `
Proxy string ` mapstructure:"proxy" `
2026-03-30 22:46:03 +03:00
}
2026-03-30 00:31:49 +03:00
type CacheConfig struct {
TTLSeconds int ` mapstructure:"ttl_seconds" `
MaxSize int ` mapstructure:"max_size" `
}
2026-03-25 23:31:41 +03:00
type ResilienceConfig struct {
MaxRetries int ` mapstructure:"max_retries" `
AllowEndpointFallback bool ` mapstructure:"allow_endpoint_fallback" `
}
type CircuitBreakerConfig struct {
Failures int ` mapstructure:"failures" `
RecoverySeconds int ` mapstructure:"recovery_seconds" `
Successes int ` mapstructure:"successes" `
}
type CORSConfig struct {
Enabled bool ` mapstructure:"enabled" `
AllowOrigins string ` mapstructure:"allow_origins" `
AllowMethods string ` mapstructure:"allow_methods" `
AllowHeaders string ` mapstructure:"allow_headers" `
MaxAge int ` mapstructure:"max_age" `
}
2026-04-20 20:12:19 +03:00
type CaptchaConfig struct {
SolverEnabled bool ` mapstructure:"solver_enabled" `
}
2023-06-30 22:49:58 +03:00
var config = Config { }
2023-06-23 04:08:00 +03:00
2025-09-30 05:27:54 +03:00
var flagToConfigKey = map [ string ] string {
2026-04-01 00:54:21 +03:00
"host" : "server.host" ,
"port" : "server.port" ,
"timeout" : "app.timeout" ,
"config" : "server.config_path" ,
2026-03-25 23:31:41 +03:00
"browser-path" : "app.browser_path" ,
2026-04-01 00:54:21 +03:00
"verbose" : "server.verbose" ,
"debug" : "server.debug" ,
"head" : "app.head" ,
"leakless" : "app.leakless" ,
"raw" : "server.raw_requests" ,
2026-03-25 23:31:41 +03:00
"leave" : "app.leave_head" ,
"2captcha_key" : "2captcha.apikey" ,
2026-04-01 00:54:21 +03:00
"proxy" : "proxies.global" ,
"stealth" : "app.stealth" ,
"insecure" : "server.insecure" ,
2026-03-30 00:31:49 +03:00
"cache_ttl" : "cache.ttl_seconds" ,
"cache_max_size" : "cache.max_size" ,
2026-03-25 23:31:41 +03:00
"max_retries" : "resilience.max_retries" ,
"allow_endpoint_fallback" : "resilience.allow_endpoint_fallback" ,
"cb_failures" : "circuit_breaker.failures" ,
"cb_recovery" : "circuit_breaker.recovery_seconds" ,
"cb_successes" : "circuit_breaker.successes" ,
2025-09-30 05:27:54 +03:00
}
2023-06-23 04:08:00 +03:00
var RootCmd = & cobra . Command {
2023-06-30 22:49:58 +03:00
Use : "openserp" ,
Short : "Open SERP" ,
Long : ` Get [Google, Yandex, Baidu] search engine results via API or CLI. ` ,
Version : version ,
SilenceUsage : true ,
2023-06-23 04:08:00 +03:00
PersistentPreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
2023-06-24 22:26:36 +03:00
err := initializeConfig ( cmd )
2023-06-30 22:49:58 +03:00
if err != nil {
return err
}
2026-04-01 00:54:21 +03:00
core . InitLogger ( config . Server . IsVerbose , config . Server . IsDebug )
2023-06-30 22:49:58 +03:00
logrus . Debugf ( "Final config: %+v" , config )
return nil
2023-06-23 04:08:00 +03:00
} ,
}
// Bind each cobra flag to its associated viper configuration (config file and environment variable)
func bindFlags ( cmd * cobra . Command , vpr * viper . Viper ) {
cmd . Flags ( ) . VisitAll ( func ( flg * pflag . Flag ) {
2025-09-30 05:27:54 +03:00
configName , ok := flagToConfigKey [ flg . Name ]
if ! ok {
configName = "app." + flg . Name
}
if err := vpr . BindPFlag ( configName , flg ) ; err != nil {
logrus . Errorf ( "Unable to bind flag %s: %v" , flg . Name , err )
}
2023-06-23 04:08:00 +03:00
2023-06-30 22:49:58 +03:00
if flg . Changed {
2025-09-30 05:27:54 +03:00
val , err := parseFlagValue ( flg )
if err != nil {
logrus . Errorf ( "Unable to parse flag %s: %v" , flg . Name , err )
return
}
vpr . Set ( configName , val )
2023-06-23 04:08:00 +03:00
}
} )
}
2025-09-30 05:27:54 +03:00
func parseFlagValue ( flg * pflag . Flag ) ( interface { } , error ) {
switch flg . Value . Type ( ) {
case "string" :
return flg . Value . String ( ) , nil
case "bool" :
return strconv . ParseBool ( flg . Value . String ( ) )
case "int" :
return strconv . Atoi ( flg . Value . String ( ) )
default :
return flg . Value . String ( ) , nil
}
}
2023-06-23 04:08:00 +03:00
// Initialize Viper
func initializeConfig ( cmd * cobra . Command ) error {
v := viper . New ( )
2026-03-25 23:31:41 +03:00
setConfigDefaults ( v )
2023-06-23 04:08:00 +03:00
2026-04-04 19:32:23 +03:00
explicitConfigPath := strings . TrimSpace ( cmd . Flag ( "config" ) . Value . String ( ) )
if explicitConfigPath == "" {
explicitConfigPath = strings . TrimSpace ( os . Getenv ( envPrefix + "_SERVER_CONFIG_PATH" ) )
}
if explicitConfigPath != "" {
v . SetConfigFile ( explicitConfigPath )
} else {
// Base name of the config file, without the file extension
v . SetConfigName ( defaultConfigFilename )
v . AddConfigPath ( "." )
}
2023-06-23 04:08:00 +03:00
2025-09-30 05:27:54 +03:00
// 1. Config file (lowest priority). Return an error if we cannot parse the config file.
2023-06-30 22:49:58 +03:00
err := v . ReadInConfig ( )
if err != nil {
2026-04-04 19:32:23 +03:00
if explicitConfigPath != "" {
return fmt . Errorf ( "cannot read config %q: %w" , explicitConfigPath , err )
}
2024-05-06 01:13:07 +03:00
err = fmt . Errorf ( "cannot read config: %v" , err )
2023-06-30 22:49:58 +03:00
logrus . Warn ( err )
2023-06-23 04:08:00 +03:00
}
2025-09-30 05:27:54 +03:00
// 2. Environment variables (medium priority). Bind environment variables to their equivalent keys with underscores
2023-06-30 22:49:58 +03:00
for _ , key := range v . AllKeys ( ) {
envKey := envPrefix + "_" + strings . ToUpper ( strings . ReplaceAll ( key , "." , "_" ) )
err := v . BindEnv ( key , envKey )
if err != nil {
logrus . Errorf ( "Unable to bind ENV valye: %v" , err )
}
}
2023-06-23 04:08:00 +03:00
2025-09-30 05:27:54 +03:00
// 3. Command flags (highest priority). Bind the current command's flags to viper
2023-06-23 04:08:00 +03:00
bindFlags ( cmd , v )
2026-04-01 00:54:21 +03:00
if err := validateRemovedConfigPaths ( v ) ; err != nil {
return err
}
2023-06-30 22:49:58 +03:00
// Dump Viper values to config struct
2026-04-01 00:54:21 +03:00
if err := validateEngineProxyTags ( v ) ; err != nil {
return err
}
2023-06-30 22:49:58 +03:00
err = v . Unmarshal ( & config )
if err != nil {
2024-05-06 01:13:07 +03:00
return fmt . Errorf ( "cannot unmarshall config: %v" , err )
2023-06-30 22:49:58 +03:00
}
2026-04-01 00:54:21 +03:00
config . Proxies , err = core . NormalizeProxiesConfig ( config . Proxies )
2026-03-30 22:46:03 +03:00
if err != nil {
2026-04-01 00:54:21 +03:00
return fmt . Errorf ( "invalid proxies config: %w" , err )
2026-03-30 22:46:03 +03:00
}
2026-04-01 00:54:21 +03:00
if config . Server . IsDebug {
logrus . Debug ( "Viper config:" )
v . Debug ( )
}
return nil
}
func validateEngineProxyTags ( v * viper . Viper ) error {
for _ , engineName := range [ ] string { "google" , "yandex" , "baidu" , "bing" , "duckduckgo" } {
key := engineName + ".proxy"
if ! v . IsSet ( key ) {
continue
}
raw := v . Get ( key )
tag , ok := raw . ( string )
if ! ok {
return fmt . Errorf ( "invalid %s.proxy config: proxy must be a string tag" , engineName )
}
if _ , err := core . NormalizeProxyTag ( tag ) ; err != nil {
return fmt . Errorf ( "invalid %s.proxy config: %w" , engineName , err )
}
2026-03-30 22:46:03 +03:00
}
2026-04-01 00:54:21 +03:00
return nil
}
func validateRemovedConfigPaths ( v * viper . Viper ) error {
legacyKeys := map [ string ] string {
"app.proxy" : "use proxies.global or proxies.entries with per-engine proxy tags instead" ,
"proxy_pool" : "use proxies.entries and proxies.health.failure_threshold instead" ,
"proxy_pool.urls" : "use proxies.entries instead" ,
"proxy_pool.failure_threshold" : "use proxies.health.failure_threshold instead" ,
"app.host" : "move to server.host" ,
"app.port" : "move to server.port" ,
"app.debug" : "move to server.debug" ,
"app.verbose" : "move to server.verbose" ,
"app.raw_requests" : "move to server.raw_requests" ,
"app.insecure" : "move to server.insecure" ,
"proxies.defaults" : "use proxies.global or per-engine proxy tags instead" ,
"proxies.defaults.mode" : "use proxies.global or per-engine proxy tags instead" ,
"proxies.defaults.tag" : "use per-engine proxy tags on each engine instead" ,
"google.proxy.mode" : "use google.proxy: <tag> or omit it for direct mode" ,
"google.proxy.tag" : "use google.proxy: <tag>" ,
"yandex.proxy.mode" : "use yandex.proxy: <tag> or omit it for direct mode" ,
"yandex.proxy.tag" : "use yandex.proxy: <tag>" ,
"baidu.proxy.mode" : "use baidu.proxy: <tag> or omit it for direct mode" ,
"baidu.proxy.tag" : "use baidu.proxy: <tag>" ,
"bing.proxy.mode" : "use bing.proxy: <tag> or omit it for direct mode" ,
"bing.proxy.tag" : "use bing.proxy: <tag>" ,
"duckduckgo.proxy.mode" : "use duckduckgo.proxy: <tag> or omit it for direct mode" ,
"duckduckgo.proxy.tag" : "use duckduckgo.proxy: <tag>" ,
2026-03-30 22:46:03 +03:00
}
2026-04-01 00:54:21 +03:00
for key , hint := range legacyKeys {
if v . IsSet ( key ) {
return fmt . Errorf ( "config key %q is removed in proxy v2: %s" , key , hint )
}
2023-06-30 22:49:58 +03:00
}
2023-06-23 04:08:00 +03:00
return nil
}
2026-03-25 23:31:41 +03:00
func setConfigDefaults ( v * viper . Viper ) {
2026-04-01 00:54:21 +03:00
v . SetDefault ( "server.host" , "127.0.0.1" )
v . SetDefault ( "server.port" , 7070 )
v . SetDefault ( "server.debug" , false )
v . SetDefault ( "server.verbose" , false )
v . SetDefault ( "server.raw_requests" , false )
v . SetDefault ( "server.insecure" , false )
v . SetDefault ( "app.timeout" , 30 )
v . SetDefault ( "app.browser_path" , "" )
v . SetDefault ( "app.head" , false )
v . SetDefault ( "app.leave_head" , false )
v . SetDefault ( "app.leakless" , false )
v . SetDefault ( "app.stealth" , false )
v . SetDefault ( "proxies.entries" , [ ] interface { } { } )
v . SetDefault ( "proxies.global" , "" )
v . SetDefault ( "proxies.health.failure_threshold" , core . DefaultProxyFailureThreshold )
2026-03-30 00:31:49 +03:00
v . SetDefault ( "cache.ttl_seconds" , 300 )
v . SetDefault ( "cache.max_size" , 1000 )
2026-03-25 23:31:41 +03:00
// Keep stage2 defaults stable even when config file is absent.
v . SetDefault ( "resilience.max_retries" , 3 )
v . SetDefault ( "resilience.allow_endpoint_fallback" , false )
v . SetDefault ( "circuit_breaker.failures" , 5 )
v . SetDefault ( "circuit_breaker.recovery_seconds" , 60 )
v . SetDefault ( "circuit_breaker.successes" , 2 )
v . SetDefault ( "cors.enabled" , true )
v . SetDefault ( "cors.allow_origins" , "*" )
v . SetDefault ( "cors.allow_methods" , "GET, POST, OPTIONS" )
2026-04-01 17:38:13 +03:00
v . SetDefault ( "cors.allow_headers" , "Origin, Content-Type, Accept, Authorization, X-Use-Proxy" )
2026-03-25 23:31:41 +03:00
v . SetDefault ( "cors.max_age" , 86400 )
2026-04-20 20:12:19 +03:00
v . SetDefault ( "captcha.solver_enabled" , false )
2026-03-25 23:31:41 +03:00
}
2023-06-23 04:08:00 +03:00
func init ( ) {
2026-04-01 00:54:21 +03:00
RootCmd . PersistentFlags ( ) . IntVarP ( & config . Server . Port , "port" , "p" , 7070 , "Port number to run server" )
RootCmd . PersistentFlags ( ) . StringVarP ( & config . Server . Host , "host" , "a" , "127.0.0.1" , "Host address to run server" )
2023-06-30 22:49:58 +03:00
RootCmd . PersistentFlags ( ) . IntVarP ( & config . App . Timeout , "timeout" , "t" , 30 , "Timeout to fail request" )
2026-04-01 00:54:21 +03:00
RootCmd . PersistentFlags ( ) . StringVarP ( & config . Server . ConfigPath , "config" , "c" , "" , "Configuration file path" )
2026-03-17 02:18:51 +03:00
RootCmd . PersistentFlags ( ) . StringVarP ( & config . App . BrowserPath , "browser-path" , "" , "" , "Custom browser binary path (Chrome/Chromium/Edge/Brave..)" )
2026-04-01 00:54:21 +03:00
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" )
2023-06-30 22:49:58 +03:00
RootCmd . PersistentFlags ( ) . BoolVarP ( & config . App . IsBrowserHead , "head" , "" , false , "Enable browser UI" )
RootCmd . PersistentFlags ( ) . BoolVarP ( & config . App . IsLeakless , "leakless" , "l" , false , "Use leakless mode to insure browser instances are closed after search" )
2026-04-01 00:54:21 +03:00
RootCmd . PersistentFlags ( ) . BoolVarP ( & config . Server . IsRawRequests , "raw" , "r" , false , "Disable browser usage, use HTTP requests" )
2023-06-30 22:49:58 +03:00
RootCmd . PersistentFlags ( ) . BoolVarP ( & config . App . IsLeaveHead , "leave" , "" , false , "Leave browser and tabs opened after search is made" )
2024-05-06 01:13:07 +03:00
RootCmd . PersistentFlags ( ) . StringVarP ( & config . Config2Capcha . ApiKey , "2captcha_key" , "" , "" , "2 captcha api key" )
2026-04-01 00:54:21 +03:00
RootCmd . PersistentFlags ( ) . StringVarP ( & config . Proxies . Global , "proxy" , "x" , "" , "Force a single proxy for all engines (same as proxies.global)" )
2025-08-18 22:49:45 +03:00
RootCmd . PersistentFlags ( ) . BoolVarP ( & config . App . IsStealth , "stealth" , "s" , false , "Use stealth browser plugin" )
2026-04-01 00:54:21 +03:00
RootCmd . PersistentFlags ( ) . BoolVarP ( & config . Server . Insecure , "insecure" , "k" , false , "Allow insecure TLS connections" )
2026-03-30 00:31:49 +03:00
RootCmd . PersistentFlags ( ) . IntVar ( & config . Cache . TTLSeconds , "cache_ttl" , 300 , "Cache TTL in seconds (0 to disable)" )
RootCmd . PersistentFlags ( ) . IntVar ( & config . Cache . MaxSize , "cache_max_size" , 1000 , "Maximum number of cached responses" )
2026-03-25 23:31:41 +03:00
RootCmd . PersistentFlags ( ) . IntVar ( & config . Resilience . MaxRetries , "max_retries" , 3 , "Max retry attempts per search engine (0 to disable)" )
RootCmd . PersistentFlags ( ) . BoolVar ( & config . Resilience . AllowEndpointFallback , "allow_endpoint_fallback" , false , "Allow dedicated endpoints to fallback to other engines" )
RootCmd . PersistentFlags ( ) . IntVar ( & config . CircuitBreaker . Failures , "cb_failures" , 5 , "Consecutive failures before circuit breaker opens" )
RootCmd . PersistentFlags ( ) . IntVar ( & config . CircuitBreaker . RecoverySeconds , "cb_recovery" , 60 , "Seconds before retrying an engine with open circuit" )
RootCmd . PersistentFlags ( ) . IntVar ( & config . CircuitBreaker . Successes , "cb_successes" , 2 , "Consecutive successful half-open checks needed to close circuit" )
2023-06-23 04:08:00 +03:00
}