From e2d59ef7e529e45c8e2966b6dbc7c16c5f96ce6d Mon Sep 17 00:00:00 2001 From: Tremendous Stimulus <295218925+tremendous-stimulus@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:21:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(ios):=20=D1=80=D1=83=D1=87=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20fallback=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20VK=20captcha=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D0=B5=D0=BD=D1=82=D0=B5=D1=80=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Когда авто-решатель captcha не справился, прокидываем в vk-провайдер ManualSolverFunc, который переиспользует SolveViaProxy и сигналит UI через gomobile-интерфейс CaptchaPresenter (Show/Hide). Без презентера поведение прежнее. Файлы библиотеки не затронуты - мостик в новом файле vk/iosmanual.go. --- internal/provider/vk/iosmanual.go | 52 +++++++++++++++++++++++++++++++ ios/captcha.go | 36 +++++++++++++++++++++ ios/mobile.go | 13 +++++--- 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 internal/provider/vk/iosmanual.go create mode 100644 ios/captcha.go diff --git a/internal/provider/vk/iosmanual.go b/internal/provider/vk/iosmanual.go new file mode 100644 index 0000000..e1e2b9f --- /dev/null +++ b/internal/provider/vk/iosmanual.go @@ -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 + } +} diff --git a/ios/captcha.go b/ios/captcha.go new file mode 100644 index 0000000..2b5578c --- /dev/null +++ b/ios/captcha.go @@ -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 +} diff --git a/ios/mobile.go b/ios/mobile.go index 5d5558c..2878a5d 100644 --- a/ios/mobile.go +++ b/ios/mobile.go @@ -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