fix(captcha): exit non-zero on misconfig, tighten solver gate tests

This commit is contained in:
Rustem Kamalov
2026-04-20 20:12:19 +03:00
parent 1842c4bb07
commit 08edec5779
14 changed files with 453 additions and 39 deletions

24
cmd/captcha_config.go Normal file
View File

@@ -0,0 +1,24 @@
package cmd
import (
"fmt"
"strings"
"github.com/sirupsen/logrus"
)
func resolveCaptchaSolverConfig() (bool, string, error) {
apiKey := strings.TrimSpace(config.Config2Capcha.ApiKey)
if !config.Captcha.SolverEnabled {
if apiKey != "" {
logrus.Warn("2captcha.apikey is set but captcha.solver_enabled=false; solver will not run")
}
return false, "", nil
}
if apiKey == "" {
return false, "", fmt.Errorf("captcha solver is enabled (captcha.solver_enabled=true) but 2captcha.apikey is empty")
}
return true, apiKey, nil
}

View File

@@ -14,7 +14,7 @@ import (
)
const (
version = "0.6.4"
version = "0.6.5"
defaultConfigFilename = "config"
envPrefix = "OPENSERP"
)
@@ -27,6 +27,7 @@ type Config struct {
Resilience ResilienceConfig `mapstructure:"resilience"`
CircuitBreaker CircuitBreakerConfig `mapstructure:"circuit_breaker"`
CORS CORSConfig `mapstructure:"cors"`
Captcha CaptchaConfig `mapstructure:"captcha"`
Config2Capcha Config2Captcha `mapstructure:"2captcha"`
GoogleConfig EngineConfig `mapstructure:"google"`
YandexConfig EngineConfig `mapstructure:"yandex"`
@@ -87,6 +88,10 @@ type CORSConfig struct {
MaxAge int `mapstructure:"max_age"`
}
type CaptchaConfig struct {
SolverEnabled bool `mapstructure:"solver_enabled"`
}
var config = Config{}
var flagToConfigKey = map[string]string{
@@ -322,6 +327,7 @@ func setConfigDefaults(v *viper.Viper) {
v.SetDefault("cors.allow_methods", "GET, POST, OPTIONS")
v.SetDefault("cors.allow_headers", "Origin, Content-Type, Accept, Authorization, X-Use-Proxy")
v.SetDefault("cors.max_age", 86400)
v.SetDefault("captcha.solver_enabled", false)
}
func init() {

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
@@ -34,6 +35,12 @@ func search(cmd *cobra.Command, args []string) {
Insecure: config.Server.Insecure,
}
captchaSolverEnabled, captchaSolverAPIKey, err := resolveCaptchaSolverConfig()
if err != nil {
logrus.Errorf("Error validating captcha solver config: %v", err)
os.Exit(1)
}
proxyRuntime := core.ProxyRuntimeBrowser
if config.Server.IsRawRequests {
proxyRuntime = core.ProxyRuntimeRaw
@@ -65,7 +72,7 @@ func search(cmd *cobra.Command, args []string) {
results, err = searchRaw(engineType, query)
} else {
logrus.Infof("Using browser mode for %s search", engineType)
results, err = searchBrowser(engineType, query, selectedProxy)
results, err = searchBrowser(engineType, query, selectedProxy, captchaSolverEnabled, captchaSolverAPIKey)
}
if err != nil {
@@ -84,7 +91,7 @@ func search(cmd *cobra.Command, args []string) {
fmt.Println(string(b))
}
func searchBrowser(engineType string, query core.Query, browserProxyURL string) ([]core.SearchResult, error) {
func searchBrowser(engineType string, query core.Query, browserProxyURL string, captchaSolverEnabled bool, captchaSolverAPIKey string) ([]core.SearchResult, error) {
var engine core.SearchEngine
if core.IsAuthenticatedSocksProxyURL(browserProxyURL) {
return nil, fmt.Errorf(
@@ -95,15 +102,16 @@ func searchBrowser(engineType string, query core.Query, browserProxyURL string)
}
opts := core.BrowserOpts{
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverApiKey: config.Config2Capcha.ApiKey,
BrowserPath: config.App.BrowserPath,
ProxyURL: browserProxyURL,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverEnabled: captchaSolverEnabled,
CaptchaSolverApiKey: captchaSolverAPIKey,
BrowserPath: config.App.BrowserPath,
ProxyURL: browserProxyURL,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
}
if config.Server.IsDebug {

View File

@@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
@@ -70,6 +71,12 @@ func serve(cmd *cobra.Command, args []string) {
corsCfg.AllowHeaders = config.CORS.AllowHeaders
corsCfg.MaxAge = config.CORS.MaxAge
captchaSolverEnabled, captchaSolverAPIKey, err := resolveCaptchaSolverConfig()
if err != nil {
logrus.Error(err)
os.Exit(1)
}
proxyRuntime := core.ProxyRuntimeBrowser
if config.Server.IsRawRequests {
proxyRuntime = core.ProxyRuntimeRaw
@@ -96,14 +103,15 @@ func serve(cmd *cobra.Command, args []string) {
}
baseOpts := core.BrowserOpts{
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverApiKey: config.Config2Capcha.ApiKey,
BrowserPath: config.App.BrowserPath,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverEnabled: captchaSolverEnabled,
CaptchaSolverApiKey: captchaSolverAPIKey,
BrowserPath: config.App.BrowserPath,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
}
if config.Server.IsDebug {
baseOpts.IsHeadless = false

View File

@@ -103,3 +103,69 @@ func TestValidateBrowserProxyPolicyRejectsTaggedAuthenticatedSocksInPool(t *test
t.Fatalf("expected explicit authenticated SOCKS error, got %v", err)
}
}
func TestResolveCaptchaSolverConfigDisabledWithoutKey(t *testing.T) {
origEnabled := config.Captcha.SolverEnabled
origKey := config.Config2Capcha.ApiKey
defer func() {
config.Captcha.SolverEnabled = origEnabled
config.Config2Capcha.ApiKey = origKey
}()
config.Captcha.SolverEnabled = false
config.Config2Capcha.ApiKey = ""
enabled, key, err := resolveCaptchaSolverConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if enabled {
t.Fatal("expected solver to be disabled")
}
if key != "" {
t.Fatalf("expected empty solver key when disabled, got %q", key)
}
}
func TestResolveCaptchaSolverConfigEnabledWithoutKeyFails(t *testing.T) {
origEnabled := config.Captcha.SolverEnabled
origKey := config.Config2Capcha.ApiKey
defer func() {
config.Captcha.SolverEnabled = origEnabled
config.Config2Capcha.ApiKey = origKey
}()
config.Captcha.SolverEnabled = true
config.Config2Capcha.ApiKey = ""
_, _, err := resolveCaptchaSolverConfig()
if err == nil {
t.Fatal("expected missing API key error")
}
if !strings.Contains(err.Error(), "captcha solver is enabled") {
t.Fatalf("expected clear startup error, got %v", err)
}
}
func TestResolveCaptchaSolverConfigEnabledWithKey(t *testing.T) {
origEnabled := config.Captcha.SolverEnabled
origKey := config.Config2Capcha.ApiKey
defer func() {
config.Captcha.SolverEnabled = origEnabled
config.Config2Capcha.ApiKey = origKey
}()
config.Captcha.SolverEnabled = true
config.Config2Capcha.ApiKey = "api-key"
enabled, key, err := resolveCaptchaSolverConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !enabled {
t.Fatal("expected solver to be enabled")
}
if key != "api-key" {
t.Fatalf("expected configured API key, got %q", key)
}
}