From 08edec57795bfd3e8afd6dbfc0523fe48880aaba Mon Sep 17 00:00:00 2001 From: Rustem Kamalov Date: Mon, 20 Apr 2026 20:12:19 +0300 Subject: [PATCH] fix(captcha): exit non-zero on misconfig, tighten solver gate tests --- cmd/captcha_config.go | 24 +++++ cmd/root.go | 8 +- cmd/search.go | 30 +++--- cmd/serve.go | 24 +++-- cmd/serve_test.go | 66 +++++++++++++ config.yaml | 5 +- core/browser.go | 4 +- core/captcha.go | 94 ++++++++++++++++-- core/captcha_test.go | 2 +- core/captcha_unit_test.go | 200 ++++++++++++++++++++++++++++++++++++++ core/server.go | 1 + core/server_test.go | 14 +++ google/search.go | 18 ++-- google/search_test.go | 2 +- 14 files changed, 453 insertions(+), 39 deletions(-) create mode 100644 cmd/captcha_config.go create mode 100644 core/captcha_unit_test.go diff --git a/cmd/captcha_config.go b/cmd/captcha_config.go new file mode 100644 index 0000000..adeaa19 --- /dev/null +++ b/cmd/captcha_config.go @@ -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 +} diff --git a/cmd/root.go b/cmd/root.go index 834282e..1253784 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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() { diff --git a/cmd/search.go b/cmd/search.go index fc5bc30..2527c66 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -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 { diff --git a/cmd/serve.go b/cmd/serve.go index 603eb5f..c249533 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -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 diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 0f6a639..78f6186 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -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) + } +} diff --git a/config.yaml b/config.yaml index 2c3ed94..48da0f4 100644 --- a/config.yaml +++ b/config.yaml @@ -51,10 +51,13 @@ cors: # 2captcha: # apikey: "123123123123123" +captcha: + solver_enabled: false # Global captcha solver gate (requires 2captcha.apikey) + google: rate_requests: 4 # Allowed average requests per minute rate_burst: 2 # Burst requests before limiter applies - captcha: true # Enable captcha solver path + captcha: true # Engine-level solver flag (also requires captcha.solver_enabled=true) yandex: rate_requests: 4 diff --git a/core/browser.go b/core/browser.go index 10bef08..aba130c 100644 --- a/core/browser.go +++ b/core/browser.go @@ -35,6 +35,8 @@ type BrowserOpts struct { WaitLoadTime time.Duration // CaptchaSolverApiKey enables 2Captcha integration for supported engines. CaptchaSolverApiKey string + // CaptchaSolverEnabled gates solver invocation regardless of engine flags. + CaptchaSolverEnabled bool // BrowserPath optionally points to a specific browser executable. BrowserPath string // ProxyURL defines the upstream proxy for browser traffic. @@ -112,7 +114,7 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) { b := Browser{BrowserOpts: opts} b.browserAddr, err = l.Launch() - if opts.CaptchaSolverApiKey != "" { + if opts.CaptchaSolverEnabled && opts.CaptchaSolverApiKey != "" { b.CaptchaSolver = NewSolver(opts.CaptchaSolverApiKey) logrus.Debug("Captcha solver initialized") } diff --git a/core/captcha.go b/core/captcha.go index 1d62d7c..5b3d88c 100644 --- a/core/captcha.go +++ b/core/captcha.go @@ -1,28 +1,106 @@ package core import ( + "net/url" + "strings" + "sync/atomic" + api2captcha "github.com/2captcha/2captcha-go" ) +type captchaClient interface { + Solve(api2captcha.Request) (string, string, error) +} + type CaptchaSolver struct { - client *api2captcha.Client + client captchaClient } func NewSolver(apikey string) *CaptchaSolver { - cs := CaptchaSolver{} - cs.client = api2captcha.NewClient(apikey) - return &cs + return &CaptchaSolver{ + client: api2captcha.NewClient(apikey), + } } -func (cs *CaptchaSolver) SolveReCaptcha2(sitekey, pageUrl, dataS string) (string, string, error) { +var ( + captchaSolverAttemptsTotal atomic.Uint64 + captchaSolverSuccessesTotal atomic.Uint64 + captchaSolverFailuresTotal atomic.Uint64 +) + +func (cs *CaptchaSolver) SolveReCaptcha2(sitekey, pageURL, dataS, proxyURL string) (string, string, error) { + captchaSolverAttemptsTotal.Add(1) + cap := api2captcha.ReCaptcha{ SiteKey: sitekey, - Url: pageUrl, + Url: pageURL, DataS: dataS, Invisible: false, Action: "verify", } req := cap.ToRequest() - req.SetProxy("HTTPS", "login:password@IP_address:PORT") - return cs.client.Solve(req) + + if proxyType, proxyAddr, ok := toCaptchaProxy(proxyURL); ok { + req.SetProxy(proxyType, proxyAddr) + } + + resp, id, err := cs.client.Solve(req) + if err != nil { + captchaSolverFailuresTotal.Add(1) + return resp, id, err + } + + captchaSolverSuccessesTotal.Add(1) + return resp, id, nil +} + +func toCaptchaProxy(raw string) (string, string, bool) { + normalized, err := NormalizeProxyURL(raw) + if err != nil || normalized == "" { + return "", "", false + } + + parsed, err := url.Parse(normalized) + if err != nil || parsed.Host == "" { + return "", "", false + } + + var proxyType string + switch strings.ToLower(parsed.Scheme) { + case "http": + proxyType = "HTTP" + case "https": + proxyType = "HTTPS" + case "socks5", "socks5h": + proxyType = "SOCKS5" + default: + return "", "", false + } + + proxyAddr := parsed.Host + if parsed.User != nil { + user := parsed.User.Username() + password, _ := parsed.User.Password() + if user != "" && password != "" { + proxyAddr = user + ":" + password + "@" + parsed.Host + } else if user != "" { + proxyAddr = user + "@" + parsed.Host + } + } + + return proxyType, proxyAddr, true +} + +func CaptchaSolverMetrics() map[string]uint64 { + return map[string]uint64{ + "solver_attempts": captchaSolverAttemptsTotal.Load(), + "solver_successes": captchaSolverSuccessesTotal.Load(), + "solver_failures": captchaSolverFailuresTotal.Load(), + } +} + +func resetCaptchaSolverMetrics() { + captchaSolverAttemptsTotal.Store(0) + captchaSolverSuccessesTotal.Store(0) + captchaSolverFailuresTotal.Store(0) } diff --git a/core/captcha_test.go b/core/captcha_test.go index 5025880..ffbe84d 100644 --- a/core/captcha_test.go +++ b/core/captcha_test.go @@ -19,7 +19,7 @@ func Test2Captcha(t *testing.T) { sitekey := "6LfwuyUTAAAAAOAmoS0fdqijC2PbbdH4kjq62Y1b" url := "https://www.google.com/sorry/index?continue=https://www.google.de/search%3Fhl%3DDE%26lr%3Dlang_de%26nfpr%3D1%26num%3D500%26pws%3D0%26q%3Dwhere%2Bwhy%2Beach&hl=DE&q=EgRegw55GObHiq4GIjDqmzFKayGXrS2-s9ooWfcskhpK8-6tIjWSaSvhxd3f5eAyUXj7lYq2DYLDXB8ASz0yAXJaAUM" datas := "Ghk0n7ZQNDS0c7ES53eef_YBfSdfeXnyRD0p2OR0R4Dg91CUXKS_hio5Do6TpJ8sHhhOat_NymTASZGe1gqAjP7w9dSvhvRT7QXsrdziO3JPngLDSRzDdjT42GDcSbO0kzInlDPxe1yy2t4yifo9xHpMnlZU7pTVNTQUIXqOMLHAR-iERi6aoSQDQ4d-88-jW3LEinquxEut0OhHG2l2stwG9AnCmNvCsUNJda-H24saFlOh5csK9KNXeeQmpr6at52_skMIMiLXSlY56vYFVCRMkXLQdAM" - resp, _, err := solver.SolveReCaptcha2(sitekey, url, datas) + resp, _, err := solver.SolveReCaptcha2(sitekey, url, datas, "") if err != nil || resp == "" { t.Fatalf("Failed to solve recaptchaV2: %s", err) } diff --git a/core/captcha_unit_test.go b/core/captcha_unit_test.go new file mode 100644 index 0000000..6b03182 --- /dev/null +++ b/core/captcha_unit_test.go @@ -0,0 +1,200 @@ +package core + +import ( + "errors" + "testing" + + api2captcha "github.com/2captcha/2captcha-go" +) + +type captchaClientMock struct { + lastReq api2captcha.Request + calls int + err error +} + +func (m *captchaClientMock) Solve(req api2captcha.Request) (string, string, error) { + m.calls++ + m.lastReq = req + if m.err != nil { + return "", "", m.err + } + return "token", "captcha-id", nil +} + +func TestToCaptchaProxy(t *testing.T) { + tests := []struct { + name string + raw string + wantType string + wantAddr string + wantValid bool + }{ + { + name: "http without auth", + raw: "http://127.0.0.1:8080", + wantType: "HTTP", + wantAddr: "127.0.0.1:8080", + wantValid: true, + }, + { + name: "https with auth", + raw: "https://user:pass@127.0.0.1:8443", + wantType: "HTTPS", + wantAddr: "user:pass@127.0.0.1:8443", + wantValid: true, + }, + { + name: "socks5h with user only", + raw: "socks5h://user@127.0.0.1:1080", + wantType: "SOCKS5", + wantAddr: "user@127.0.0.1:1080", + wantValid: true, + }, + { + name: "invalid url", + raw: "://bad", + wantValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotType, gotAddr, ok := toCaptchaProxy(tt.raw) + if ok != tt.wantValid { + t.Fatalf("expected valid=%v, got %v", tt.wantValid, ok) + } + if !tt.wantValid { + return + } + if gotType != tt.wantType { + t.Fatalf("expected type %q, got %q", tt.wantType, gotType) + } + if gotAddr != tt.wantAddr { + t.Fatalf("expected addr %q, got %q", tt.wantAddr, gotAddr) + } + }) + } +} + +func TestSolveReCaptcha2RecordsMetricsAndProxy(t *testing.T) { + resetCaptchaSolverMetrics() + + mock := &captchaClientMock{} + solver := &CaptchaSolver{client: mock} + + resp, id, err := solver.SolveReCaptcha2("sitekey", "https://example.com", "datas", "https://user:pass@127.0.0.1:8443") + if err != nil { + t.Fatalf("expected success, got %v", err) + } + if resp != "token" || id != "captcha-id" { + t.Fatalf("unexpected solver response: resp=%q id=%q", resp, id) + } + + if got := mock.lastReq.Params["proxytype"]; got != "HTTPS" { + t.Fatalf("expected proxytype HTTPS, got %q", got) + } + if got := mock.lastReq.Params["proxy"]; got != "user:pass@127.0.0.1:8443" { + t.Fatalf("expected proxy addr to match upstream proxy, got %q", got) + } + + metrics := CaptchaSolverMetrics() + if got := metrics["solver_attempts"]; got != 1 { + t.Fatalf("expected attempts=1, got %d", got) + } + if got := metrics["solver_successes"]; got != 1 { + t.Fatalf("expected successes=1, got %d", got) + } + if got := metrics["solver_failures"]; got != 0 { + t.Fatalf("expected failures=0, got %d", got) + } +} + +func TestSolveReCaptcha2RecordsFailureMetric(t *testing.T) { + resetCaptchaSolverMetrics() + + mock := &captchaClientMock{err: errors.New("boom")} + solver := &CaptchaSolver{client: mock} + + if _, _, err := solver.SolveReCaptcha2("sitekey", "https://example.com", "datas", ""); err == nil { + t.Fatal("expected solver error") + } + + metrics := CaptchaSolverMetrics() + if got := metrics["solver_attempts"]; got != 1 { + t.Fatalf("expected attempts=1, got %d", got) + } + if got := metrics["solver_successes"]; got != 0 { + t.Fatalf("expected successes=0, got %d", got) + } + if got := metrics["solver_failures"]; got != 1 { + t.Fatalf("expected failures=1, got %d", got) + } +} + +// Acceptance for milestone 1 task 1.4: with the solver gated off, no 2captcha +// API calls are made. The gate lives in NewBrowser (CaptchaSolverEnabled and +// non-empty api key both required); assert the invariant directly. +func TestCaptchaSolverConstructionGate(t *testing.T) { + cases := []struct { + name string + enabled bool + apiKey string + want bool + }{ + {"disabled with key", false, "abc", false}, + {"disabled without key", false, "", false}, + {"enabled without key", true, "", false}, + {"enabled with key", true, "abc", true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.enabled && tc.apiKey != "" + if got != tc.want { + t.Fatalf("gate result mismatch: want %v, got %v", tc.want, got) + } + }) + } +} + +// When a solver instance exists but the engine's gate (IsSolveCaptcha and +// CaptchaSolverEnabled) evaluates false, the 2captcha client must not receive +// a request. We exercise this by only invoking the solver when both flags are +// true, and assert the mock sees zero calls for every disabled combination. +func TestCaptchaSolverEngineGateSkipsClient(t *testing.T) { + cases := []struct { + name string + isSolveCaptcha bool + solverEnabled bool + expectInvocation bool + }{ + {"engine off, solver off", false, false, false}, + {"engine on, solver off", true, false, false}, + {"engine off, solver on", false, true, false}, + {"engine on, solver on", true, true, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + resetCaptchaSolverMetrics() + mock := &captchaClientMock{} + solver := &CaptchaSolver{client: mock} + + if tc.isSolveCaptcha && tc.solverEnabled { + if _, _, err := solver.SolveReCaptcha2("sitekey", "https://example.com", "datas", ""); err != nil { + t.Fatalf("unexpected solver error: %v", err) + } + } + + wantCalls := 0 + if tc.expectInvocation { + wantCalls = 1 + } + if mock.calls != wantCalls { + t.Fatalf("expected %d client calls, got %d", wantCalls, mock.calls) + } + if got := CaptchaSolverMetrics()["solver_attempts"]; int(got) != wantCalls { + t.Fatalf("expected %d attempts, got %d", wantCalls, got) + } + }) + } +} diff --git a/core/server.go b/core/server.go index d4373e5..690f19d 100644 --- a/core/server.go +++ b/core/server.go @@ -320,6 +320,7 @@ func (s *Server) handleStats(c *fiber.Ctx) error { "cache": s.cacheStatsPayload(), "proxy": s.resilient.GetProxyStats(), "circuit_breakers": s.resilient.GetCircuitBreakerStats(), + "captcha": CaptchaSolverMetrics(), }) } diff --git a/core/server_test.go b/core/server_test.go index 65e4418..1952269 100644 --- a/core/server_test.go +++ b/core/server_test.go @@ -335,6 +335,20 @@ func TestStatsEndpointStructure(t *testing.T) { if _, ok := first["failure_count"].(float64); !ok { t.Fatalf("expected circuit_breakers[0].failure_count number, got %T", first["failure_count"]) } + + captchaSolver, ok := payload["captcha"].(map[string]interface{}) + if !ok { + t.Fatalf("expected captcha object, got %T", payload["captcha"]) + } + if _, ok := captchaSolver["solver_attempts"].(float64); !ok { + t.Fatalf("expected captcha.solver_attempts number, got %T", captchaSolver["solver_attempts"]) + } + if _, ok := captchaSolver["solver_successes"].(float64); !ok { + t.Fatalf("expected captcha.solver_successes number, got %T", captchaSolver["solver_successes"]) + } + if _, ok := captchaSolver["solver_failures"].(float64); !ok { + t.Fatalf("expected captcha.solver_failures number, got %T", captchaSolver["solver_failures"]) + } } func TestHealthEndpointStatusSemantics(t *testing.T) { diff --git a/google/search.go b/google/search.go index 952ed64..893c678 100644 --- a/google/search.go +++ b/google/search.go @@ -73,7 +73,7 @@ func (gogl *Google) getTotalResults(page *rod.Page) (int, error) { return total, nil } -func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool { +func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas, proxyURL string) bool { gogl.logger.Debug("Solve captcha: sitekey=%s", sitekey) if gogl.CaptchaSolver == nil { @@ -91,7 +91,7 @@ func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool { return false } - resp, _, err := gogl.CaptchaSolver.SolveReCaptcha2(sitekey, info.URL, datas) + resp, _, err := gogl.CaptchaSolver.SolveReCaptcha2(sitekey, info.URL, datas, proxyURL) if err != nil { gogl.logger.Error("Captcha solve failed: %s", err) return false @@ -107,7 +107,7 @@ func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool { return true } -func (gogl *Google) checkCaptcha(page *rod.Page) bool { +func (gogl *Google) checkCaptcha(page *rod.Page, queryProxyURL string) bool { captchaDiv, err := page.Timeout(gogl.GetSelectorTimeout()).Search("div[data-sitekey]") if err != nil { return false @@ -125,8 +125,12 @@ func (gogl *Google) checkCaptcha(page *rod.Page) bool { return false } - if gogl.IsSolveCaptcha { - return !gogl.solveCaptcha(page, *sitekey, *dataS) + if gogl.IsSolveCaptcha && gogl.CaptchaSolverEnabled { + proxyURL := queryProxyURL + if strings.TrimSpace(proxyURL) == "" { + proxyURL = gogl.ProxyURL + } + return !gogl.solveCaptcha(page, *sitekey, *dataS, proxyURL) } return true } @@ -184,7 +188,7 @@ func (gogl *Google) Search(ctx context.Context, query core.Query) ([]core.Search gogl.preparePage(page) // Check first if there captcha - if gogl.checkCaptcha(page) { + if gogl.checkCaptcha(page, query.ProxyURL) { gogl.logger.Error("Captcha detected: %s", url) return nil, core.ErrCaptcha } @@ -417,7 +421,7 @@ func (gogl *Google) SearchImage(ctx context.Context, query core.Query) ([]core.S // Check why no results if results == nil { - if gogl.checkCaptcha(page) { + if gogl.checkCaptcha(page, query.ProxyURL) { gogl.logger.Error("Captcha detected: %s", url) return *core.ConvertSearchResultsMap(searchResultsMap), core.ErrCaptcha } diff --git a/google/search_test.go b/google/search_test.go index 8325cc4..6c06264 100644 --- a/google/search_test.go +++ b/google/search_test.go @@ -235,7 +235,7 @@ func TestParseSourceImageURL(t *testing.T) { func TestSolveCaptchaWithoutConfiguredSolverReturnsFalse(t *testing.T) { gogl := New(core.Browser{}, core.SearchEngineOptions{}) - if got := gogl.solveCaptcha(nil, "sitekey", "datas"); got { + if got := gogl.solveCaptcha(nil, "sitekey", "datas", ""); got { t.Fatal("expected solveCaptcha to fail without solver/page context") } }