feat(ios): ручной fallback решения VK captcha через презентер приложения

Когда авто-решатель captcha не справился, прокидываем в vk-провайдер
ManualSolverFunc, который переиспользует SolveViaProxy и сигналит UI через
gomobile-интерфейс CaptchaPresenter (Show/Hide). Без презентера поведение
прежнее. Файлы библиотеки не затронуты - мостик в новом файле vk/iosmanual.go.
This commit is contained in:
Tremendous Stimulus
2026-06-23 23:21:08 +03:00
parent 72c65c51ed
commit e2d59ef7e5
3 changed files with 97 additions and 4 deletions
+52
View File
@@ -0,0 +1,52 @@
package vk
import (
"context"
"fmt"
"net"
"sync"
"github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/captcha"
manualcaptcha "github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/captcha/manual"
)
// iosManualURL — адрес локального прокси manual-captcha, который поднимает
// SolveViaProxy. Должен совпадать с портом из пакета manual (captchaListenPort).
// Приложение наводит на него WebView, пока пользователь решает captcha.
const iosManualURL = "http://127.0.0.1:8765"
// iosManualMu сериализует ручное решение captcha: пользователю показываем
// одновременно только одно окно, даже если в captcha упёрлись сразу несколько
// стримов/кредов.
var iosManualMu sync.Mutex
// IOSManualSolver строит ManualSolverFunc для мобильного приложения. Извлечение
// success_token переиспользует manualcaptcha.SolveViaProxy — ту же логику, что и
// десктопный CLI (локальный прокси сам ловит токен из ответов VK). show
// вызывается с URL прокси перед показом captcha (приложение должно открыть
// WebView на этот адрес), hide — после завершения, чтобы закрыть окно.
//
// Функция предназначена только для ios-биндинга и нигде в библиотеке/CLI не
// вызывается; поведение vk-провайдера для прочих вызывающих не меняется.
func IOSManualSolver(show func(url string), hide func()) ManualSolverFunc {
return func(ctx context.Context, e *captcha.Error, dialer net.Dialer) (string, string, error) {
if e.RedirectURI == "" {
return "", "", fmt.Errorf("manual captcha: no redirect_uri")
}
// Один WebView за раз: SolveViaProxy слушает фиксированный порт, да и
// показывать пользователю несколько captcha разом смысла нет.
iosManualMu.Lock()
defer iosManualMu.Unlock()
if show != nil {
show(iosManualURL)
}
if hide != nil {
defer hide()
}
token, err := manualcaptcha.SolveViaProxy(ctx, e.RedirectURI, dialer)
return token, "", err
}
}
+36
View File
@@ -0,0 +1,36 @@
package ios
import "sync"
// CaptchaPresenter — мост в UI приложения для ручного решения captcha. gomobile
// генерирует из этого интерфейса ObjC-протокол IosCaptchaPresenter, который
// реализует Swift. Вызывается из Go, когда авто-решатель captcha не справился и
// нужен ручной fallback.
//
// - Show(url): открыть WebView на локальном прокси-адресе url, где пользователь
// решает captcha. Метод блокирующим быть не обязан.
// - Hide(): captcha решена или отменена — закрыть окно.
type CaptchaPresenter interface {
Show(url string)
Hide()
}
var (
captchaMu sync.RWMutex
captchaPresenter CaptchaPresenter
)
// SetCaptchaPresenter регистрирует UI-презентер ручной captcha. Передайте nil,
// чтобы отключить ручной fallback — тогда при провале авто-captcha поток просто
// упадёт, как раньше. Вызывать один раз при старте приложения, до Start.
func SetCaptchaPresenter(p CaptchaPresenter) {
captchaMu.Lock()
captchaPresenter = p
captchaMu.Unlock()
}
func currentCaptchaPresenter() CaptchaPresenter {
captchaMu.RLock()
defer captchaMu.RUnlock()
return captchaPresenter
}
+9 -4
View File
@@ -193,9 +193,14 @@ func Start(link, peer, dns, listen, transport, obfKey string) error {
var connectedStreams atomic.Int32
// nil solver — manual captcha через браузер на мобильном недоступна.
// При captcha-блоке поток упадёт с ErrFatalNoStreams; пользователь
// должен переподключиться позже.
// Если приложение зарегистрировало презентер — включаем ручной fallback
// captcha (авто-решатель пробуется первым, при провале показываем WebView).
// Без презентера solver == nil: при captcha-блоке поток упадёт, как раньше.
var solver vk.ManualSolverFunc
if p := currentCaptchaPresenter(); p != nil {
solver = vk.IOSManualSolver(p.Show, p.Hide)
}
prov, err := vk.New(vk.Config{
// ParseClient гарантирует непустой Links (иначе вернул бы ошибку выше).
// Мобайл передаёт одну ссылку, провайдер берёт один join-код.
@@ -204,7 +209,7 @@ func Start(link, peer, dns, listen, transport, obfKey string) error {
StreamsPerCache: cfg.VK.StreamsPerCred,
StreamsAlive: connectedStreams.Load,
Log: logger,
}, nil)
}, solver)
if err != nil {
finalErr = fmt.Errorf("provider: %v", err)
return