mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
fix(captcha): не жечь персону, когда страница не дошла до вердикта
This commit is contained in:
@@ -42,6 +42,8 @@ var (
|
||||
errCaptchaRateLimit = errors.New("captcha session rate limit reached")
|
||||
errCaptchaBot = errors.New("captcha bot challenge")
|
||||
|
||||
ErrUnavailable = errors.New("captcha unavailable")
|
||||
|
||||
captchaMaxAttempts = 2
|
||||
)
|
||||
|
||||
@@ -148,12 +150,14 @@ func Solve(
|
||||
started: time.Now(),
|
||||
}
|
||||
|
||||
var solveErr error
|
||||
for attempt := 1; attempt <= captchaMaxAttempts; attempt++ {
|
||||
token, solveErr := s.solveOnce(captchaErr)
|
||||
var token string
|
||||
token, solveErr = s.solveOnce(captchaErr)
|
||||
if solveErr == nil {
|
||||
return token, nil
|
||||
}
|
||||
l.Warnf("[STREAM %d] [Captcha] solve attempt %d failed: %v", streamID, attempt, solveErr)
|
||||
l.Debugf("[STREAM %d] [Captcha] solve attempt %d failed: %v", streamID, attempt, solveErr)
|
||||
// Повторяем только то, что не дошло до check.
|
||||
if s.checked || errors.Is(solveErr, errCaptchaRateLimit) || errors.Is(solveErr, errCaptchaBot) {
|
||||
return "", solveErr
|
||||
@@ -168,7 +172,7 @@ func Solve(
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("captcha attempts exhausted")
|
||||
return "", fmt.Errorf("captcha attempts exhausted: %w", solveErr)
|
||||
}
|
||||
|
||||
func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) {
|
||||
@@ -184,7 +188,7 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) {
|
||||
|
||||
page, err := parseCaptchaPage(html)
|
||||
if err != nil {
|
||||
s.logger().Warnf("[Captcha] page parse failed: %v; pow script near: %s", err, powSnippet(html))
|
||||
s.logger().Debugf("[Captcha] page parse failed: %v (bytes=%d); pow script near: %s", err, len(html), powSnippet(html))
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -384,7 +388,7 @@ func captchaDomainFromRedirectURI(redirectURI string) string {
|
||||
}
|
||||
|
||||
func (s *captchaSession) fetchCaptchaHTML(redirectURI string) (string, error) {
|
||||
body, err := s.doRaw(fhttp.MethodGet, redirectURI, nil, map[string]string{
|
||||
body, status, err := s.doRawStatus(fhttp.MethodGet, redirectURI, nil, map[string]string{
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Sec-Fetch-Dest": "document",
|
||||
@@ -398,17 +402,20 @@ func (s *captchaSession) fetchCaptchaHTML(redirectURI string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if status < 200 || status > 299 {
|
||||
return "", fmt.Errorf("%w: captcha page http %d (bytes=%d)", ErrUnavailable, status, len(body))
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func parseCaptchaPage(html string) (*captchaPage, error) {
|
||||
pow, err := parsePowParams(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %w", ErrUnavailable, err)
|
||||
}
|
||||
debugInfo := parseCaptchaDebugInfo(html)
|
||||
if debugInfo == "" {
|
||||
return nil, errors.New("captcha debug_info not found on page")
|
||||
return nil, fmt.Errorf("%w: captcha debug_info not found on page", ErrUnavailable)
|
||||
}
|
||||
return &captchaPage{Pow: pow, DebugInfo: debugInfo, Init: parseCaptchaInitGlobal(html)}, nil
|
||||
}
|
||||
@@ -661,13 +668,23 @@ func (s *captchaSession) doRaw(
|
||||
form [][2]string,
|
||||
extraHeaders map[string]string,
|
||||
) ([]byte, error) {
|
||||
data, _, err := s.doRawStatus(method, endpoint, form, extraHeaders)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (s *captchaSession) doRawStatus(
|
||||
method string,
|
||||
endpoint string,
|
||||
form [][2]string,
|
||||
extraHeaders map[string]string,
|
||||
) ([]byte, int, error) {
|
||||
var body []byte
|
||||
if form != nil {
|
||||
body = []byte(captchaEncodeForm(form))
|
||||
}
|
||||
req, err := fhttp.NewRequestWithContext(s.ctx, method, endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Sec-Fetch-Site", "same-site")
|
||||
@@ -689,7 +706,7 @@ func (s *captchaSession) doRaw(
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
s.logger().Debugf("[Captcha] http %s %s failed t=%s after=%s %s form=%s err=%v", method, SafeURL(endpoint), s.elapsed(), time.Since(start).Truncate(time.Millisecond), navSummary(req), captchaFormSummary(form), err)
|
||||
return nil, err
|
||||
return nil, 0, fmt.Errorf("%w: %w", ErrUnavailable, err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil {
|
||||
@@ -698,7 +715,10 @@ func (s *captchaSession) doRaw(
|
||||
}()
|
||||
data, readErr := io.ReadAll(resp.Body)
|
||||
s.logger().Debugf("[Captcha] http %s %s status=%d bytes=%d t=%s after=%s %s form=%s", method, SafeURL(endpoint), resp.StatusCode, len(data), s.elapsed(), time.Since(start).Truncate(time.Millisecond), navSummary(req), captchaFormSummary(form))
|
||||
return data, readErr
|
||||
if readErr != nil {
|
||||
return nil, resp.StatusCode, fmt.Errorf("%w: %w", ErrUnavailable, readErr)
|
||||
}
|
||||
return data, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// Навигационный контекст: с какой страницы виджет якобы ходит в API. Формат
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -269,3 +270,11 @@ func TestParseCaptchaDebugInfoAmbiguous(t *testing.T) {
|
||||
t.Fatalf("debug_info = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCaptchaPageRejectsNonCaptchaHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := parseCaptchaPage("<html><head><title>429 Too Many Requests</title></head><body></body></html>")
|
||||
if !errors.Is(err, ErrUnavailable) {
|
||||
t.Fatalf("err = %v, want ErrUnavailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/logx"
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/browserprofile"
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/captcha"
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/personanet"
|
||||
"github.com/samosvalishe/free-turn-proxy/internal/randx"
|
||||
|
||||
@@ -270,7 +271,7 @@ func (c *Client) fetch(ctx context.Context, link string, streamID int) (string,
|
||||
|
||||
if errors.Is(err, ErrCaptchaWaitRequired) || errors.Is(err, ErrFatalCaptchaNoStreams) ||
|
||||
errors.Is(err, ErrInvalidJoinLink) || errors.Is(err, ErrAnonymousBlocked) ||
|
||||
errors.Is(err, ErrCallFull) {
|
||||
errors.Is(err, ErrCallFull) || errors.Is(err, captcha.ErrUnavailable) {
|
||||
return "", "", nil, err
|
||||
}
|
||||
es := err.Error()
|
||||
|
||||
@@ -3,6 +3,7 @@ package vkauth
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -145,3 +146,25 @@ func TestFetchBoundsPersonaRestarts(t *testing.T) {
|
||||
t.Fatalf("tokenChain called %d times, want %d", calls, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaUnavailableKeepsPersona(t *testing.T) {
|
||||
c := newTestClient(t, nil)
|
||||
c.autoSolver = func(context.Context, *captcha.Error, int, tlsclient.HttpClient, browserprofile.Profile) (string, error) {
|
||||
return "", fmt.Errorf("%w: captcha page http 429 (bytes=120)", captcha.ErrUnavailable)
|
||||
}
|
||||
|
||||
kept := c.currentPersona()
|
||||
_, err := c.solveCaptcha(context.Background(), nil, kept, 1, "lnk", "name", "t1", testCaptchaErr())
|
||||
if !errors.Is(err, captcha.ErrUnavailable) {
|
||||
t.Fatalf("err = %v, want captcha.ErrUnavailable", err)
|
||||
}
|
||||
if c.currentPersona().VisitorID != kept.VisitorID {
|
||||
t.Fatal("persona burned on an unavailable captcha page")
|
||||
}
|
||||
if c.captchaAttempt != 0 {
|
||||
t.Fatalf("solve-mode ladder advanced to %d", c.captchaAttempt)
|
||||
}
|
||||
if c.LockoutUntilUnix() != 0 {
|
||||
t.Fatal("lockout engaged on an unavailable captcha page")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,12 @@ func (c *Client) solveCaptcha(
|
||||
return "", solveErr
|
||||
}
|
||||
|
||||
if solveErr != nil && errors.Is(solveErr, captcha.ErrUnavailable) {
|
||||
c.captchaAttempt = attempt
|
||||
c.log.Warnf("[STREAM %d] [Captcha] captcha unavailable, persona kept: %v", streamID, solveErr)
|
||||
return "", solveErr
|
||||
}
|
||||
|
||||
if solveErr != nil {
|
||||
c.log.Warnf("[STREAM %d] [Captcha] %s failed (attempt %d): %v",
|
||||
streamID, CaptchaSolveModeLabel(solveMode), attempt+1, solveErr)
|
||||
|
||||
Reference in New Issue
Block a user