Documentation
¶
Overview ¶
Captcha solvers for the VK anonymous-login flow.
Three flavours, picked at runtime:
AutoProxyCaptchaSolver — spins up a local 127.0.0.1:<port> reverse-proxy + auto-launches the operator's default browser. Default for CLI/GUI clients running on a workstation.
PreSolvedCaptchaSolver — replays a token captured out-of-band. One-shot.
AdminWebviewCaptchaSolver — delegates to an AdminCaptchaBroker hosted by the goloom admin panel. The operator opens the broker's proxy URL in their browser, solves the captcha, and the token flows back through the same channel. Fits headless production servers where there's no desktop session for AutoProxy.
All three converge on the same internal handler tree (reverse-proxy + JS shim + token capture); the only differences are where the handlers are mounted (own ephemeral HTTP server vs. an existing one) and how the URL is delivered to the operator (auto-open browser vs. admin-panel badge).
Index ¶
- Variables
- func AdminCaptchaProxyURL(urlPrefix string, target *neturl.URL) string
- func AdminWebviewCaptchaSolver(broker AdminCaptchaBroker, tag string, lg *log.Logger) sfu.VKCaptchaSolver
- func AutoProxyCaptchaSolver(timeout time.Duration, lg *log.Logger, sink CaptureSink) sfu.VKCaptchaSolver
- func AutoProxyCaptchaSolverWithOpener(timeout time.Duration, lg *log.Logger, sink CaptureSink, openURL func(string)) sfu.VKCaptchaSolver
- func MountAdminCaptchaProxy(mux *http.ServeMux, urlPrefix string, target *neturl.URL, onToken func(string), ...) error
- func NewLoggingTransport(base http.RoundTripper, sink CaptureSink, lg *log.Logger) http.RoundTripper
- func PreSolvedCaptchaSolver(token string) sfu.VKCaptchaSolver
- func SolveCaptchaV2(ctx context.Context, challenge sfu.VKCaptchaChallenge, profile BrowserProfile, ...) (string, error)
- func WithReplaySolver(store *ProfileStore, base sfu.VKCaptchaSolver, lg *log.Logger) sfu.VKCaptchaSolver
- type AdminCaptchaBroker
- type BrowserProfile
- type CaptureSink
- type CapturedProfile
- type MountAdminCaptchaProxyOptions
- type ProfileStore
- type ProfileStoreOptions
Constants ¶
This section is empty.
Variables ¶
var ErrCaptchaV2Bot = errors.New("vkcalls: captcha v2: bot challenge")
ErrCaptchaV2Bot — VK увидел bot-сигнал на checkbox-попытке. С данным профилем не пройдёт; caller должен MarkFail и пробовать другой профиль или фоллбэкнуться.
var ErrCaptchaV2RateLimit = errors.New("vkcalls: captcha v2: rate limit reached")
ErrCaptchaV2RateLimit — VK сообщил error_limit; смысла ретраить нет.
var ErrSliderUnsupported = errors.New("vkcalls: captcha v2: slider show_type not supported (S1c-checkbox)")
ErrSliderUnsupported возвращается из SolveCaptchaV2 когда VK отдал show_type=slider. S1c-checkbox-only; caller должен fallback'нуть на интерактивный solver.
var ProfileList = []BrowserProfile{
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Google Chrome";v="145"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"macOS"`,
},
{
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Linux"`,
},
}
Functions ¶
func AdminCaptchaProxyURL ¶
AdminCaptchaProxyURL returns the relative URL where a freshly- registered challenge will live, given the broker's URL prefix scheme ("/captcha-proxy/<id>"). Convenience for callers that build challenge IDs externally.
func AdminWebviewCaptchaSolver ¶
func AdminWebviewCaptchaSolver(broker AdminCaptchaBroker, tag string, lg *log.Logger) sfu.VKCaptchaSolver
AdminWebviewCaptchaSolver returns a sfu.VKCaptchaSolver that delegates to the goloom admin panel via the supplied broker. Use this for inbounds running on headless servers where AutoProxy can't open a browser.
`tag` is a human-readable label (e.g. the inbound's tag) shown in the admin UI's pending-captcha badge.
The solver blocks until the operator solves the challenge or ctx is cancelled — under normal conditions the admin's pending-captcha badge surfaces the request within a second of the inbound asking.
func AutoProxyCaptchaSolver ¶
func AutoProxyCaptchaSolver(timeout time.Duration, lg *log.Logger, sink CaptureSink) sfu.VKCaptchaSolver
AutoProxyCaptchaSolver returns a sfu.VKCaptchaSolver that opens a local reverse-proxy + the user's default browser, then waits for the success_token to flow back through the proxy. Default for CLI tooling on a workstation.
sink — опциональный CaptureSink (обычно *ProfileStore); если задан, перехваченные device/browser_fp/UA отправятся туда для будущего auto-replay. Передавай nil если не используешь стор.
func AutoProxyCaptchaSolverWithOpener ¶
func AutoProxyCaptchaSolverWithOpener(timeout time.Duration, lg *log.Logger, sink CaptureSink, openURL func(string)) sfu.VKCaptchaSolver
AutoProxyCaptchaSolverWithOpener — то же что AutoProxyCaptchaSolver, но использует custom URL opener. Мобильные клиенты передают сюда функцию которая шлёт URL в native WebView (см. mobile/api.go); desktop-вариант идёт через [openBrowser] (system browser).
func MountAdminCaptchaProxy ¶
func MountAdminCaptchaProxy(mux *http.ServeMux, urlPrefix string, target *neturl.URL, onToken func(string), opts MountAdminCaptchaProxyOptions) error
MountAdminCaptchaProxy installs the captcha proxy + JS shim handlers under urlPrefix on mux. urlPrefix must start with "/" and not end with "/" (e.g. "/captcha-proxy/abc123"). target is the upstream URL (id.vk.com/not_robot_captcha?session_token=…). onToken fires when a success_token is captured (server-side or via the JS shim's POST fallback).
Если opts.FingerprintSink задан — поверх defaultTransport натягивается [loggingTransport], который параллельно с обычным proxying извлекает device/browser_fp/UA и кормит их в sink (для будущего auto-replay, см. ProfileStore).
Used by internal/admin.CaptchaBroker; sites without an admin server should use AutoProxyCaptchaSolver instead.
func NewLoggingTransport ¶
func NewLoggingTransport(base http.RoundTripper, sink CaptureSink, lg *log.Logger) http.RoundTripper
NewLoggingTransport возвращает обёртку. base = http транспорт, который реально шлёт запросы (обычно [defaultTransport]).
func PreSolvedCaptchaSolver ¶
func PreSolvedCaptchaSolver(token string) sfu.VKCaptchaSolver
PreSolvedCaptchaSolver returns a one-shot solver that hands back a pre-captured success_token. Useful when an external orchestrator (admin webview, mobile app) has already solved the captcha and the caller just needs to feed the token into the auth chain.
Returns an error on the second invocation — tokens are single-use.
func SolveCaptchaV2 ¶
func SolveCaptchaV2(ctx context.Context, challenge sfu.VKCaptchaChallenge, profile BrowserProfile, saved *CapturedProfile, lg *log.Logger) (string, error)
SolveCaptchaV2 пытается пройти captcha автоматически по captured-profile из пула. Возвращает success_token при успехе.
challenge — то что прилетело из getAnonymousToken (sid, redirect_uri, session_token). profile — текущий браузерный профиль для UA + sec-ch-ua-* (тот же что и у auth-ладдера; брать из AuthResult.Profile). saved — captured-profile из ProfileStore.Pick(); определяет device + browser_fp + UA для replay'я (если не nil — replay; если nil — генерируется свежий browser_fp но device берётся из default'а, шансов меньше).
func WithReplaySolver ¶
func WithReplaySolver(store *ProfileStore, base sfu.VKCaptchaSolver, lg *log.Logger) sfu.VKCaptchaSolver
Types ¶
type AdminCaptchaBroker ¶
type AdminCaptchaBroker interface {
// Register adds a pending challenge to the broker's queue and
// returns the URL the operator should open in their browser to
// solve it, plus a channel that fires once with the captured
// success_token (empty string on timeout/cancel).
//
// `tag` is a human-readable label (typically the inbound tag) so
// the admin UI can show "captcha needed for <tag>" instead of
// an opaque hex id.
//
// Implementations must respect ctx — when ctx is cancelled, the
// channel must be closed so the solver doesn't hang.
Register(ctx context.Context, ch sfu.VKCaptchaChallenge, tag string) (proxyURL string, done <-chan string)
}
AdminCaptchaBroker is the admin-side surface that AdminWebviewCaptchaSolver delegates to. The admin package implements this with a registry of pending challenges + HTTP routes mounted on the existing admin server.
type BrowserProfile ¶
type BrowserProfile struct {
UserAgent string
SecChUa string
SecChUaMobile string
SecChUaPlatform string
}
BrowserProfile — спаренные UA и Client Hints. VK-WAF матчит UA против sec-ch-ua-*: рассогласование («Chrome/146 в UA, но sec-ch-ua-platform: macOS на Windows-UA») — сильный bot-сигнал.
Список лифтнут из vk-turn-proxy PR #162 client/profiles.go::ProfileList (5 swap'ов из 10 — оставляем самые свежие, чтобы заявленный Chrome version совпадал с реальным major-релизом).
func PickProfile ¶
func PickProfile() BrowserProfile
PickProfile возвращает случайный профиль. Один DoAuth должен использовать один профиль на все 4 запроса — иначе UA «сменился между шагами», что само по себе bot-сигнал.
type CaptureSink ¶
type CaptureSink interface {
Capture(p CapturedProfile)
}
CaptureSink — куда сбрасываются перехваченные fingerprint'ы. Реализуется ProfileStore.Capture; абстракция нужна чтобы loggingTransport не зависел от ProfileStore напрямую (легче тестировать + AutoProxy путь может писать в другое место).
type CapturedProfile ¶
type CapturedProfile struct {
ID string `json:"id"` // 16-hex-чисел; для индексации
UserAgent string `json:"user_agent"` // как в `User-Agent` HTTP-заголовке
DeviceJSON string `json:"device_json"` // raw JSON из form-поля `device`
BrowserFP string `json:"browser_fp"` // hex-строка из form-поля `browser_fp`
CapturedAt time.Time `json:"captured_at"`
LastUsedAt time.Time `json:"last_used_at,omitempty"`
Successes int `json:"successes"`
Failures int `json:"failures"`
// ConsecutiveFails — фейлы подряд с момента последнего успеха
// (сбрасывается в 0 на MarkSuccess). Это и есть «подряд» для
// DropAfterFailures: профиль с большой историей успехов, который VK
// внезапно забанил, должен вылетать через N фейлов подряд, а не
// ждать пока Failures догонит Successes.
ConsecutiveFails int `json:"consecutive_fails,omitempty"`
}
CapturedProfile — один захваченный fingerprint из браузера.
type MountAdminCaptchaProxyOptions ¶
type MountAdminCaptchaProxyOptions struct {
FingerprintSink CaptureSink
Logger *log.Logger
}
MountAdminCaptchaProxyOptions конфигурирует MountAdminCaptchaProxy.
FingerprintSink — опциональный сборщик device/browser_fp/UA, перехватывает тела `captchaNotRobot.componentDone` и `.check` пока они проходят через reverse-proxy. Передаётся обычно как `*ProfileStore`; nil → перехвата нет (legacy-поведение).
Logger — лог для loggingTransport diagnostics; nil → silent.
type ProfileStore ¶
type ProfileStore struct {
// contains filtered or unexported fields
}
ProfileStore — пул CapturedProfile с persistence.
Параметры пула (из решения по открытым вопросам в poc/docs/vk-calls-redesign.md):
- Capacity: cap пула (10–20). При переполнении вытесняется худший профиль (max Failures, затем oldest LastUsedAt).
- Cooldown: минимальное время между двумя реюзами одного профиля. Если все профили в cooldown'е — Pick возвращает freshest anyway, лучше попытаться чем фолбэкнуть на ручной solve.
- DropAfterFailures: 2 fail подряд → выбрасываем.
func NewProfileStore ¶
func NewProfileStore(opts ProfileStoreOptions) (*ProfileStore, error)
NewProfileStore создаёт стор и загружает существующие записи с диска. Если файла нет — стартует с пустым пулом. Если файл битый — возвращает ошибку (бить файл программно мы не должны, выгоднее дать оператору починить вручную).
func (*ProfileStore) Capture ¶
func (s *ProfileStore) Capture(p CapturedProfile)
Capture сохраняет fingerprint в пул.
Если профиль с такими же device_json + browser_fp уже есть — апдейтится CapturedAt (актуализация freshness), счётчики не сбрасываем. Иначе вставляется новая запись; если пул переполнен — вытесняется худший.
Пустые поля (UA / DeviceJSON / BrowserFP) → silently no-op, не загрязняем пул мусором.
func (*ProfileStore) MarkFail ¶
func (s *ProfileStore) MarkFail(id string)
MarkFail отмечает captcha-fail. После dropAfterFailures фейлов ПОДРЯД (без успеха между ними) — удаляем профиль из пула: VK его уже flag'нул, дальнейшие попытки только жгут session_token'ы и упираются в интерактивный fallback. Считаем именно подряд (ConsecutiveFails), чтобы профиль с богатой историей успехов, который VK внезапно забанил, вылетал быстро, а не ждал пока Failures догонит Successes.
func (*ProfileStore) MarkSuccess ¶
func (s *ProfileStore) MarkSuccess(id string)
MarkSuccess отмечает профиль как успешно прошедший captcha. LastUsedAt обновляется, Successes++.
func (*ProfileStore) Pick ¶
func (s *ProfileStore) Pick() *CapturedProfile
Pick возвращает наиболее подходящий профиль для следующей попытки auto-solve или nil если пул пуст. Стратегия:
- Профили в cooldown'е (LastUsedAt < now-cooldown) — приоритет.
- Среди cooldown-passed — берём с минимумом fail/success ratio, ties break'аем oldest LastUsedAt (LRU — даём отдохнуть свежим).
- Если все в cooldown'е — берём наиболее «отдохнувший» (oldest LastUsedAt) — лучше попытаться чем фолбэкнуть.
Не помечает использование — это делает MarkSuccess/MarkFail в зависимости от исхода.
func (*ProfileStore) Snapshot ¶
func (s *ProfileStore) Snapshot() []CapturedProfile
Snapshot возвращает копию текущего пула — для admin UI / тестов.