captcha

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: GPL-3.0 Imports: 21 Imported by: 0

Documentation

Overview

manual.go — консольный решатель (задача E2): тот, который работает всегда.

Окна у него нет и браузером он не управляет — только печатает ссылку и принимает ответ. Взамен он не требует ни WebView2 Runtime, ни Chromium и работает на всех трёх платформах с первого дня. Два удобных решателя, которые появятся позже, опираются на него как на запасное дно (doc/e2-captcha.md, §7).

Почему у пользователя приходится просить возни с консолью браузера, а не просто ссылку, на которую его перенаправило: редиректа с токеном у VK нет вовсе, токен приходит в теле ответа captchaNotRobot.check. Подробности — в token.go и в том же документе.

open.go — открыть ссылку в браузере пользователя.

Живёт здесь, а не в CLI, потому что пользуется этим только капча: ссылку на страницу проверки печатает и открывает именно она. Решателю функция передаётся полем ManualOptions.Open — запускать посторонние программы должен тот, кто знает, хочет ли этого пользователь.

proxy.go — локальный прокси для окна капчи при kill-switch (plan-e3.md, W2).

Сеть окна WebView2 делает msedgewebview2.exe, а не tt, и при kill-switch («закрыто») она закрыта. Разрешать его по процессу нельзя: тем же бинарём пользуются Teams, Outlook, виджеты, и они пошли бы мимо туннеля. Поэтому окно ходит через этот прокси, а он — сокетами tt, которые kill-switch пропускает: диалер с хуком netguard и резолвер ядра.

Прокси слушает loopback, и до него может достучаться любой процесс машины. Чтобы он не стал дырой в kill-switch, пускает он только CONNECT на порт 443 доменов VK (proxyDomains) и живёт, пока открыто окно.

Package captcha — внешний решатель Smart Captcha VK.

Сама капча решается в трёх местах, и два из них нам не принадлежат: нативный решатель Go v2 внутри ядра закрывает большинство случаев и сюда не приходит вовсе (`creds.go`, шаги 1 и 3 таблицы из architecture.md §08). Наружу ядро выходит только тогда, когда не справилось само: отправляет core.EventCaptchaRequired и ждёт ответа с таймаутом.

solver.go — каркас (задача E1): интерфейс Solver, разбор события в Request и Dispatcher, который связывает одно с другим и отвечает ядру через core.Control.SubmitCaptcha. Сами решатели живут рядом: manual.go — ручной режим (E2), webview2_windows.go — окно WebView2 на Windows (E3). Какой из них взять, решает CLI.

token.go — то, что знают про капчу все решатели сразу: как выглядит ответ VK и как заставить страницу отдать токен.

Файл общий намеренно. Токен добывается из одного и того же места тремя способами — консольным (manual.go), окном WebView2 и браузером под управлением по CDP (doc/e2-captcha.md), — и знание об ответе VK должно жить в одном экземпляре, иначе оно разъедется.

webview2_other.go — заглушка окна капчи там, где WebView2 нет.

На Linux и macOS удобный решатель — браузер под управлением по CDP, и он отнесён к Э4 (doc/e2-captcha.md §7). До тех пор здесь работает консольный.

Index

Constants

View Source
const DefaultHumanWindow = 30 * time.Second

DefaultHumanWindow — меньше этого срока человека просить бессмысленно.

Десятисекундное окно ModeAuto закрывается отказом сразу, и это не потеря, а выигрыш: в автоцепочке ядра таких окон два подряд, и без немедленного отказа каждая капча стоила бы двадцати секунд пустого ожидания, прежде чем ядро дойдёт до ручного режима, где у человека есть минута.

View Source
const DefaultMargin = time.Second

DefaultMargin — запас, на который решатель обязан ответить раньше, чем ядро перестанет слушать. Токен, пришедший после таймаута, не просто бесполезен: ядро уже пошло дальше, а лишний ответ останется в канале и будет выброшен перед следующим запросом (core.Control.SubmitCaptcha).

Variables

View Source
var ErrBusy = errors.New("решатель уже занят другой капчей")

ErrBusy — решатель уже занят другой капчей.

View Source
var ErrCancelled = errors.New("капча не решена: ответа нет")

ErrCancelled — пользователь отказался решать капчу: пустой ответ или конец ввода.

View Source
var ErrNoSolver = errors.New("внешний решатель капчи не задан")

ErrNoSolver — решателя нет вовсе. Ядру такой отказ отправляется немедленно.

View Source
var ErrNoToken = errors.New("в ответе VK нет success_token")

ErrNoToken — в ответе не нашлось ни токена, ни внятной ошибки.

View Source
var ErrProxy = errors.New("прокси для окна капчи не поднялся")

ErrProxy — прокси для окна не поднялся, окно не открывалось. CLI по нему открывает окно без прокси, сняв kill-switch на время капчи.

View Source
var ErrSlider = errors.New("VK показал слайдер")

ErrSlider — VK показал слайдер вместо чекбокса. Отдельная ошибка, потому что у неё другой смысл: решать можно и нужно, просто дольше. Ядро на Android по этому признаку переключало стратегию (CaptchaWebViewManager.kt, ERROR_SLIDER_DETECTED).

View Source
var ErrTooShort = errors.New("окна ядра не хватит на ручное решение")

ErrTooShort — до конца ожидания ядра осталось меньше, чем нужно человеку.

Functions

func OpenInBrowser

func OpenInBrowser(rawURL string) error

OpenInBrowser открывает ссылку средствами системы.

Принимаются только http и https. Проверка не формальность: строка приходит от VK и уезжает в командную строку чужой программы, а схемы вроде file: или javascript: там значат совсем не то, что мы имели в виду.

Types

type DialFunc

type DialFunc = func(ctx context.Context, network, address string) (net.Conn, error)

DialFunc — чем прокси соединяется с сайтом: DialContext у net.Dialer.

type Dispatcher

type Dispatcher struct {
	// contains filtered or unexported fields
}

Dispatcher принимает события ядра и отвечает на них через решатель.

Подключается в тот же Sink, что автомат супервизора: Observe разбирает core.EventCaptchaRequired, остальные события пропускает.

func New

func New(opts Options) *Dispatcher

New собирает диспетчер. Решатель до первого события не трогается.

func (*Dispatcher) Close

func (d *Dispatcher) Close()

Close отменяет незаконченное решение и дожидается его. Безопасен при повторном вызове.

func (*Dispatcher) Observe

func (d *Dispatcher) Observe(ev core.Event)

Observe — получатель событий ядра. Возвращается сразу: решение уходит в отдельную горутину.

В фон это вынесено не ради скорости, а потому что иначе всё встало бы: Observe зовётся из Sink, а Sink супервизора держит на время вызова замок вывода (lifecycle.go). Решатель, ждущий человека минуту, заморозил бы на эту минуту весь вывод клиента.

type Manual

type Manual struct {
	// contains filtered or unexported fields
}

Manual — консольный решатель.

func NewManual

func NewManual(opts ManualOptions) *Manual

NewManual собирает решатель. Ввод и вывод не трогаются до Solve.

func (*Manual) NeedsHuman

func (*Manual) NeedsHuman() bool

NeedsHuman: да, и это главное про него. Диспетчер по этому признаку не станет звать его на десятисекундные окна автоцепочки.

func (*Manual) Solve

func (m *Manual) Solve(ctx context.Context, req Request) (string, error)

Solve печатает инструкцию и ждёт ответа.

type ManualOptions

type ManualOptions struct {
	// In — откуда читается ответ. nil — os.Stdin.
	In io.Reader

	// Out — куда уходят приглашение и инструкция. nil — os.Stderr.
	//
	// Именно stderr: stdout у CLI занят тем, что является результатом команды,
	// и `tt connect > log.txt` не должен прятать вопрос от пользователя.
	Out io.Writer

	// Open — открыть ссылку в браузере. nil означает только напечатать её.
	//
	// Полем, а не встроенным вызовом: запускать посторонние программы — решение
	// CLI, а не библиотеки, и тестам это не нужно. Готовая реализация рядом —
	// OpenInBrowser.
	Open func(rawURL string) error
}

ManualOptions — настройки консольного решателя.

type Mode

type Mode string

Mode — чем ядро занималось до того, как обратилось наружу. От режима зависит только одно, зато важное: сколько ядро согласно ждать ответа.

const (
	// ModeAuto — автоцепочка не прошла, идёт запасной путь WBV Auto.
	// Ядро ждёт десять секунд: человеку этого не хватит заведомо.
	ModeAuto Mode = "auto"
	// ModeManual — автоцепочка кончилась, ядро открывает ручной режим.
	// Минута на всё: открыть ссылку, решить капчу, вернуть токен.
	ModeManual Mode = "manual"
	// ModeSelected — режим wv выбран настройками, ядро сразу идёт наружу.
	// Две минуты.
	ModeSelected Mode = "selected"
)

type Options

type Options struct {
	// Solver — решатель. nil допустим и означает, что капчу решать некому:
	// ядро получит отказ немедленно, без ожидания.
	Solver Solver

	// Control — ручка ядра, через которую уходит ответ. nil допустим (вызовы
	// core.Control безопасны на nil), но тогда ответ никуда не уйдёт.
	Control *core.Control

	// Logf — куда писать; nil означает молчать. Как и у datapath, свой вывод
	// пакет не заводит: он принадлежит CLI.
	Logf func(format string, args ...any)

	// Margin — насколько раньше срока ядра обязан ответить решатель.
	// 0 — DefaultMargin.
	Margin time.Duration

	// HumanWindow — окно, короче которого не стоит просить человека.
	// 0 — DefaultHumanWindow.
	HumanWindow time.Duration
}

Options — настройки диспетчера.

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

Proxy — локальный HTTP-прокси (только CONNECT) для окна капчи.

func StartProxy

func StartProxy(dial DialFunc, logf func(string, ...any)) (*Proxy, error)

StartProxy поднимает прокси на 127.0.0.1 со случайным портом. logf получает строку на каждый отказ — по ним видно, чего странице не хватило; nil — молчать.

func (*Proxy) Addr

func (p *Proxy) Addr() string

Addr — адрес прокси, «127.0.0.1:порт».

func (*Proxy) Close

func (p *Proxy) Close() error

Close закрывает прокси и все соединения через него и ждёт их горутин.

type Request

type Request struct {
	// Session — номер воркера, который просит. Воркеров бывает до 108, и знать,
	// чей запрос на экране, полезно.
	Session int
	// Mode — чем ядро занималось до обращения наружу.
	Mode Mode
	// RedirectURI — страница капчи VK, которую нужно открыть.
	RedirectURI string
	// SessionToken — токен сессии капчи; без него ответ не примут.
	SessionToken string
	// Deadline — момент, после которого ответ уже не нужен. Решателю его стоит
	// показать: человеку важно знать, сколько у него времени.
	Deadline time.Time
}

Request — просьба решить капчу, разобранная из core.EventCaptchaRequired.

func (Request) Left

func (r Request) Left(now time.Time) time.Duration

Left — сколько осталось до конца ожидания ядра.

type Solver

type Solver interface {
	Solve(ctx context.Context, req Request) (string, error)

	// NeedsHuman — решателю нужен человек. От этого зависит, браться ли за
	// короткие окна: десять секунд машине хватит, а человеку нет.
	NeedsHuman() bool
}

Solver решает капчу и возвращает токен.

Ошибка означает «не решил»; ядро получит отказ и пойдёт дальше своей цепочкой. Пустой токен без ошибки читается так же — обещать успех и не дать токен нельзя.

Solve обязан уважать ctx: он несёт и срок ядра, и остановку клиента по Ctrl+C. Решателю, который ждёт человека, этого мало — прочитать строку со stdin по отмене ctx стандартными средствами невозможно, — поэтому ждать ввод он должен в отдельной горутине, а возвращаться по ctx.Done() (см. manual.go).

type WebView2

type WebView2 struct{}

WebView2 — решатель с окном; вне Windows его не бывает.

func NewWebView2

func NewWebView2(WebView2Options) (*WebView2, error)

NewWebView2 вне Windows всегда отказывает. Отказ обёрнут в errors.ErrUnsupported: CLI по нему молча берёт консольный решатель, не предупреждая о том, чего на этой системе и не ждут.

func (*WebView2) NeedsHuman

func (*WebView2) NeedsHuman() bool

NeedsHuman — как у настоящего.

func (*WebView2) Solve

Solve недостижим: NewWebView2 экземпляра не возвращает.

type WebView2Options

type WebView2Options struct {
	Out  io.Writer
	Dial DialFunc
	Logf func(format string, args ...any)
}

WebView2Options — настройки решателя с окном. См. webview2_windows.go.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL