Documentation
¶
Overview ¶
browser_linux.go — капча в браузере под управлением по CDP (Linux).
WebView2 на Linux нет, а встроенный движок (WebKitGTK, Qt WebEngine, CEF) требует cgo и системных библиотек. Поэтому qwdtt-cli запускает установленный браузер на Chromium со своим временным профилем, открывает в нём страницу капчи и забирает токен по протоколу DevTools (cdp.go). Человек только решает капчу — как в окне WebView2 на Windows.
Управление — по трубе (--remote-debugging-pipe), дескрипторы 3 и 4 через ExtraFiles: порта нет, и чужой процесс к браузеру не подключится. Нет и чтения DevToolsActivePort из профиля — а у Chromium из snap свой /tmp, и профиль, созданный нами, он бы там не увидел.
cdp.go — разговор с браузером по протоколу DevTools (CDP): открыть страницу капчи и снять токен из ответа VK, не трогая саму страницу.
Транспорт — труба (--remote-debugging-pipe): браузер читает команды из дескриптора 3 и пишет ответы в дескриптор 4, сообщения — JSON, каждое заканчивается нулевым байтом. Ни порта, ни WebSocket: подключиться к такому браузеру чужой процесс не может. Запуск браузера — browser_linux.go; здесь только протокол, без ОС, поэтому и тесты его идут на записанных сообщениях где угодно.
Токен снимается на сетевом уровне, а не перехватчиком JS, как в окне WebView2: Network.responseReceived для captchaNotRobot.check → Network.loadingFinished → Network.getResponseBody → successTokenFrom. Страница при этом не изменена ни на байт (doc/e2-captcha.md, §4).
manual.go — консольный решатель (задача E2): тот, который работает всегда.
Окна у него нет и браузером он не управляет — только печатает ссылку и принимает ответ. Взамен он не требует ни WebView2 Runtime, ни Chromium и работает на всех трёх платформах с первого дня. Два удобных решателя, которые появятся позже, опираются на него как на запасное дно (doc/e2-captcha.md, §7).
Почему у пользователя приходится просить возни с консолью браузера, а не просто ссылку, на которую его перенаправило: редиректа с токеном у VK нет вовсе, токен приходит в теле ответа captchaNotRobot.check. Подробности — в token.go и в том же документе.
open.go — открыть ссылку в браузере пользователя.
Живёт здесь, а не в CLI, потому что пользуется этим только капча: ссылку на страницу проверки печатает и открывает именно она. Решателю функция передаётся полем ManualOptions.Open — запускать посторонние программы должен тот, кто знает, хочет ли этого пользователь.
profile.go — временные профили браузеров капчи: окна WebView2 и браузера под управлением по CDP. Профиль у каждой капчи свой и пустой — это замена clearCaptchaCookies() Android: чистая сессия по построению.
proxy.go — локальный прокси для окна капчи при kill-switch (plan-e3.md, W2).
Сеть окна WebView2 делает msedgewebview2.exe, а не qwdtt-cli, и при kill-switch («закрыто») она закрыта. Разрешать его по процессу нельзя: тем же бинарём пользуются Teams, Outlook, виджеты, и они пошли бы мимо туннеля. Поэтому окно ходит через этот прокси, а он — сокетами qwdtt-cli, которые 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), browser_linux.go — браузер под управлением по CDP на Linux. Какой из них взять, решает CLI.
token.go — то, что знают про капчу все решатели сразу: как выглядит ответ VK и как заставить страницу отдать токен.
Файл общий намеренно. Токен добывается из одного и того же места тремя способами — консольным (manual.go), окном WebView2 и браузером под управлением по CDP (doc/e2-captcha.md), — и знание об ответе VK должно жить в одном экземпляре, иначе оно разъедется.
webview2_other.go — заглушка окна капчи там, где WebView2 нет.
На Linux окно капчи даёт браузер под управлением по CDP (browser_linux.go), на macOS — только консольный решатель.
Index ¶
Constants ¶
const DefaultHumanWindow = 30 * time.Second
DefaultHumanWindow — меньше этого срока человека просить бессмысленно.
Десятисекундное окно ModeAuto закрывается отказом сразу, и это не потеря, а выигрыш: в автоцепочке ядра таких окон два подряд, и без немедленного отказа каждая капча стоила бы двадцати секунд пустого ожидания, прежде чем ядро дойдёт до ручного режима, где у человека есть минута.
const DefaultMargin = time.Second
DefaultMargin — запас, на который решатель обязан ответить раньше, чем ядро перестанет слушать. Токен, пришедший после таймаута, не просто бесполезен: ядро уже пошло дальше, а лишний ответ останется в канале и будет выброшен перед следующим запросом (core.Control.SubmitCaptcha).
Variables ¶
var ErrBrowser = errors.New("браузер для капчи не запустился")
ErrBrowser — браузер не запустился или не открыл страницу капчи. Человек капчи ещё не видел, и CLI по этой ошибке решает ту же капчу в ручном режиме.
var ErrBusy = errors.New("решатель уже занят другой капчей")
ErrBusy — решатель ещё занят прошлой капчей, хотя ядро её уже не ждёт.
var ErrCancelled = errors.New("капча не решена: ответа нет")
ErrCancelled — пользователь отказался решать капчу: пустой ответ или конец ввода.
var ErrNoBrowser = errors.New("браузер на Chromium не найден")
ErrNoBrowser — браузера на Chromium нет. CLI по нему берёт ручной режим.
var ErrNoSolver = errors.New("внешний решатель капчи не задан")
ErrNoSolver — решателя нет вовсе. Ядру такой отказ отправляется немедленно.
var ErrNoToken = errors.New("в ответе VK нет success_token")
ErrNoToken — в ответе не нашлось ни токена, ни внятной ошибки.
var ErrProxy = errors.New("прокси для окна капчи не поднялся")
ErrProxy — прокси для окна не поднялся, окно не открывалось. CLI по нему открывает окно без прокси, сняв kill-switch на время капчи.
var ErrSlider = errors.New("VK показал слайдер")
ErrSlider — VK показал слайдер вместо чекбокса. Отдельная ошибка, потому что у неё другой смысл: решать можно и нужно, просто дольше. Ядро на Android по этому признаку переключало стратегию (CaptchaWebViewManager.kt, ERROR_SLIDER_DETECTED).
var ErrTooShort = errors.New("окна ядра не хватит на ручное решение")
ErrTooShort — до конца ожидания ядра осталось меньше, чем нужно человеку.
var ErrWindowClosed = errors.New("окно капчи закрыто")
ErrWindowClosed — человек закрыл окно капчи, не решив её. Ядро получит отказ сразу и пойдёт дальше, а не выстоит весь таймаут.
Functions ¶
func OpenInBrowser ¶
OpenInBrowser открывает ссылку средствами системы.
Принимаются только http и https. Проверка не формальность: строка приходит от VK и уезжает в командную строку чужой программы, а схемы вроде file: или javascript: там значат совсем не то, что мы имели в виду.
Types ¶
type Browser ¶ added in v1.0.0
type Browser struct {
// contains filtered or unexported fields
}
Browser — решатель в браузере под управлением.
func NewBrowser ¶ added in v1.0.0
func NewBrowser(opts BrowserOptions) (*Browser, error)
NewBrowser находит браузер на Chromium и собирает решатель. Браузер до первой капчи не запускается. Ошибка — ErrNoBrowser.
func (*Browser) NeedsHuman ¶ added in v1.0.0
NeedsHuman: да — решает человек, браузер только показывает страницу.
type BrowserOptions ¶ added in v1.0.0
type BrowserOptions struct {
// Out — куда уходит сообщение, что браузер открыт. nil — os.Stderr.
Out io.Writer
// Dial, если задан, — браузер ходит в сеть только через локальный прокси
// qwdtt-cli (proxy.go), а он соединяется этой функцией: при kill-switch
// страница грузится сокетами qwdtt-cli. Прокси не поднялся — Solve
// отказывает с ErrProxy, браузера не запуская.
Dial DialFunc
// Logf — строки прокси и разговора с браузером; nil — молчать.
Logf func(format string, args ...any)
}
BrowserOptions — настройки решателя с браузером.
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 ¶
NeedsHuman: да, и это главное про него. Диспетчер по этому признаку не станет звать его на десятисекундные окна автоцепочки.
type ManualOptions ¶
type ManualOptions struct {
// In — откуда читается ответ. nil — os.Stdin.
In io.Reader
// Out — куда уходят приглашение и инструкция. nil — os.Stderr.
//
// Именно stderr: stdout у CLI занят тем, что является результатом команды,
// и `qwdtt-cli connect > log.txt` не должен прятать вопрос от пользователя.
Out io.Writer
// Open — открыть ссылку в браузере. nil означает только напечатать её.
//
// Полем, а не встроенным вызовом: запускать посторонние программы — решение
// CLI, а не библиотеки, и тестам это не нужно. Готовая реализация рядом —
// OpenInBrowser.
Open func(rawURL string) error
// Hint — строка, которую стоит показать вместе с инструкцией: например,
// чем ручной режим можно заменить. Пусто — ничего.
Hint string
}
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
// Stop — остановка клиента (Ctrl+C). С её отменой решатель получает
// отмену, а ядро — отказ, не дожидаясь Close. Это не то же, что Close в
// defer: ядро ждёт ответа на капчу без своего ctx, до трёх минут, и
// остановка туннеля ждёт его воркер — до defer дело бы не дошло. nil —
// только Close.
Stop context.Context
}
Options — настройки диспетчера.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy — локальный HTTP-прокси (только CONNECT) для окна капчи.
func StartProxy ¶
StartProxy поднимает прокси на 127.0.0.1 со случайным портом. logf получает строку на каждый отказ — по ним видно, чего странице не хватило; nil — молчать.
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.
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 по нему молча берёт консольный решатель, не предупреждая о том, чего на этой системе и не ждут.