mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e009db0b9a | |||
| 321a0995c3 | |||
| 6f934f10f4 | |||
| ea410a044a | |||
| b4189f8f43 |
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.26.4"
|
||||
GO_VERSION: "1.26.5"
|
||||
GOLANGCI_VERSION: v2.12.2
|
||||
GOVULN_VERSION: v1.1.4
|
||||
GORELEASER_VERSION: v2.15.4
|
||||
|
||||
@@ -10,7 +10,7 @@ permissions:
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.26.4"
|
||||
GO_VERSION: "1.26.5"
|
||||
GORELEASER_VERSION: v2.15.4
|
||||
GOMOBILE_VERSION: latest
|
||||
TASK_VERSION: 3.51.1
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "1.7.2"
|
||||
".": "1.7.3"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [1.7.3](https://github.com/samosvalishe/free-turn-proxy/compare/v1.7.2...v1.7.3) (2026-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **captcha:** адаптировать авторешение под SPA-капчу VK ([b4189f8](https://github.com/samosvalishe/free-turn-proxy/commit/b4189f8f4317b1c5c919ab8e0e183ebfb927d8be))
|
||||
* **deps:** обновить Go 1.26.4 -> 1.26.5 ([ea410a0](https://github.com/samosvalishe/free-turn-proxy/commit/ea410a044ad1ebd3fbc14286f41213357a6732f4))
|
||||
|
||||
## [1.7.2](https://github.com/samosvalishe/free-turn-proxy/compare/v1.7.1...v1.7.2) (2026-07-07)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/samosvalishe/free-turn-proxy
|
||||
|
||||
go 1.26.4
|
||||
go 1.26.5
|
||||
|
||||
require (
|
||||
github.com/bogdanfinn/fhttp v0.6.8
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
fhttp "github.com/bogdanfinn/fhttp"
|
||||
@@ -36,7 +35,6 @@ func SetLogger(l logx.Logger) { Log = logx.OrNop(l) }
|
||||
|
||||
const (
|
||||
captchaAPIVersion = "5.131"
|
||||
captchaScriptVersion = "1.1.1374"
|
||||
captchaAPIOrigin = "https://id.vk.ru"
|
||||
captchaDomain = "vk.ru"
|
||||
captchaDeviceInfo = `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1032,"innerWidth":1147,"innerHeight":945,"devicePixelRatio":1,"language":"ru-RU","languages":["ru-RU"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":16,"connectionEffectiveType":"4g","notificationsPermission":"denied"}`
|
||||
@@ -46,19 +44,13 @@ const (
|
||||
var (
|
||||
reCaptchaPowInput = regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`)
|
||||
reCaptchaDifficulty = regexp.MustCompile(`const\s+difficulty\s*=\s*(\d+)`)
|
||||
reCaptchaWindowInit = regexp.MustCompile(`(?s)window\.init\s*=\s*(\{.*?})\s*;`)
|
||||
reCaptchaScriptSrc = regexp.MustCompile(`src="(https://[^"]+not_robot_captcha[^"]+)"`)
|
||||
reCaptchaDebugInfo = regexp.MustCompile(`debug_info:(?:[^"]*\|\|)?"([a-fA-F0-9]{64})"`)
|
||||
reCaptchaHex64 = regexp.MustCompile(`"([a-fA-F0-9]{64})"`)
|
||||
reCaptchaVersion = regexp.MustCompile(`vkid/([0-9.]*)/not_robot_captcha\.js`)
|
||||
reCaptchaDebugUUID = regexp.MustCompile(`[a-zA-Z0-9_]{6,}:\s*"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"`)
|
||||
|
||||
errCaptchaRateLimit = errors.New("captcha session rate limit reached")
|
||||
errCaptchaBot = errors.New("captcha bot challenge")
|
||||
|
||||
captchaMaxAttempts = 2
|
||||
|
||||
captchaDebugCache sync.Map // scriptURL -> string
|
||||
captchaVersionSeen sync.Map // script version -> struct{} (drift warned once)
|
||||
captchaHeaderOrder = []string{
|
||||
"host",
|
||||
"content-length",
|
||||
@@ -80,15 +72,6 @@ var (
|
||||
captchaPHeaderOrder = []string{":method", ":path", ":authority", ":scheme"}
|
||||
)
|
||||
|
||||
type captchaInit struct {
|
||||
Data captchaInitData `json:"data"`
|
||||
}
|
||||
|
||||
type captchaInitData struct {
|
||||
ShowCaptchaType string `json:"show_captcha_type"`
|
||||
CaptchaSettings []captchaInitSetting `json:"captcha_settings"`
|
||||
}
|
||||
|
||||
type captchaInitSetting struct {
|
||||
Type string `json:"type"`
|
||||
Settings string `json:"settings"`
|
||||
@@ -98,8 +81,7 @@ type captchaInitSetting struct {
|
||||
type captchaPage struct {
|
||||
PowInput string
|
||||
PowDifficulty int
|
||||
ScriptURL string
|
||||
Init *captchaInit
|
||||
DebugInfo string
|
||||
}
|
||||
|
||||
type captchaCheck struct {
|
||||
@@ -184,60 +166,46 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) {
|
||||
|
||||
page, err := parseCaptchaPage(html)
|
||||
if err != nil {
|
||||
s.dumpChunks("html", html)
|
||||
return "", err
|
||||
}
|
||||
s.logCaptchaPage(page)
|
||||
if page.PowInput == "" {
|
||||
return "", errors.New("failed to find PoW settings")
|
||||
}
|
||||
|
||||
sliderContent := captchaContentRef{}
|
||||
if page.Init != nil {
|
||||
for _, setting := range page.Init.Data.CaptchaSettings {
|
||||
if setting.Type == "slider" {
|
||||
sliderContent = setting.contentRef()
|
||||
}
|
||||
}
|
||||
}
|
||||
if page.Init != nil && page.Init.Data.ShowCaptchaType == "slider" && sliderContent.Value == "" {
|
||||
return "", errors.New("failed to find slider captcha settings key")
|
||||
}
|
||||
|
||||
s.logger().Debugf("[Captcha] solving pow difficulty=%d", page.PowDifficulty)
|
||||
hash := solveCaptchaPoW(s.ctx, page.PowInput, page.PowDifficulty)
|
||||
if hash == "" {
|
||||
return "", errors.New("captcha pow failed")
|
||||
}
|
||||
s.logger().Debugf("[Captcha] pow solved hash_len=%d", len(hash))
|
||||
|
||||
base := s.captchaBaseValues(captchaErr.SessionToken)
|
||||
if _, settingsErr := s.captchaRequest("captchaNotRobot.settings", base); settingsErr != nil {
|
||||
return "", fmt.Errorf("captcha settings failed: %w", settingsErr)
|
||||
debugInfo := page.DebugInfo
|
||||
if debugInfo == "" {
|
||||
debugInfo = newCaptchaUUID()
|
||||
s.logger().Warnf("[Captcha] debug_info uuid not in HTML; generated fallback")
|
||||
}
|
||||
|
||||
browserFP, err := captchaBrowserFP()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
browserFPSource := "generated"
|
||||
if s.savedProfile != nil && strings.TrimSpace(s.savedProfile.BrowserFp) != "" {
|
||||
browserFP = s.savedProfile.BrowserFp
|
||||
browserFPSource = "saved"
|
||||
}
|
||||
s.logger().Debugf("[Captcha] browser_fp source=%s len=%d", browserFPSource, len(browserFP))
|
||||
|
||||
s.warnVersionDrift(page.ScriptURL)
|
||||
base := s.captchaBaseValues(captchaErr.SessionToken)
|
||||
if _, settingsErr := s.captchaRequest("captchaNotRobot.settings", base); settingsErr != nil {
|
||||
return "", fmt.Errorf("captcha settings failed: %w", settingsErr)
|
||||
}
|
||||
|
||||
debugInfo, err := s.fetchDebugInfo(page.ScriptURL)
|
||||
initResp, err := s.captchaRequest("captchaNotRobot.initSession", [][2]string{
|
||||
{"session_token", captchaErr.SessionToken},
|
||||
{"domain", s.domain},
|
||||
{"lang", "0"},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch debug info: %w (script_version=%s)", err, captchaScriptVersion)
|
||||
return "", fmt.Errorf("captcha initSession failed: %w", err)
|
||||
}
|
||||
showType, sliderContent := parseCaptchaInitSession(initResp)
|
||||
s.logger().Debugf("[Captcha] initSession show_type=%q slider_len=%d", showType, len(sliderContent.Value))
|
||||
|
||||
showType := ""
|
||||
if page.Init != nil {
|
||||
showType = page.Init.Data.ShowCaptchaType
|
||||
}
|
||||
s.logger().Debugf("[Captcha] solving show_type=%s", showType)
|
||||
var token string
|
||||
switch showType {
|
||||
case "slider":
|
||||
@@ -257,6 +225,36 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func parseCaptchaInitSession(raw map[string]any) (string, captchaContentRef) {
|
||||
resp, ok := raw["response"].(map[string]any)
|
||||
if !ok {
|
||||
return "", captchaContentRef{}
|
||||
}
|
||||
showType := captchaStringifyAny(resp["show_captcha_type"])
|
||||
content := captchaContentRef{}
|
||||
if data, err := json.Marshal(resp["content_settings"]); err == nil {
|
||||
var settings []captchaInitSetting
|
||||
if json.Unmarshal(data, &settings) == nil {
|
||||
for _, setting := range settings {
|
||||
if setting.Type == "slider" {
|
||||
content = setting.contentRef()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return showType, content
|
||||
}
|
||||
|
||||
func newCaptchaUUID() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
|
||||
// captchaContentRef несёт значение настроек слайдера.
|
||||
type captchaContentRef struct {
|
||||
Source string
|
||||
@@ -315,101 +313,38 @@ func (s *captchaSession) fetchCaptchaHTML(redirectURI string) (string, error) {
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (s *captchaSession) fetchDebugInfo(scriptURL string) (string, error) {
|
||||
if cached, ok := captchaDebugCache.Load(scriptURL); ok {
|
||||
if cachedDebugInfo, ok := cached.(string); ok {
|
||||
return cachedDebugInfo, nil
|
||||
}
|
||||
captchaDebugCache.Delete(scriptURL)
|
||||
func (s *captchaSession) dumpChunks(label, body string) {
|
||||
const chunk = 1800
|
||||
total := (len(body) + chunk - 1) / chunk
|
||||
l := s.logger()
|
||||
l.Debugf("[Captcha][DUMP] %s bytes=%d chunks=%d", label, len(body), total)
|
||||
for i := range total {
|
||||
start := i * chunk
|
||||
end := min(start+chunk, len(body))
|
||||
l.Debugf("[Captcha][DUMP %s %d/%d] %s", label, i+1, total, body[start:end])
|
||||
}
|
||||
body, err := s.doRaw(fhttp.MethodGet, scriptURL, nil, map[string]string{
|
||||
"Accept": "text/javascript,*/*",
|
||||
"Referer": captchaAPIOrigin + "/",
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
v, usedFallback, err := extractDebugInfo(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if usedFallback {
|
||||
s.logger().Warnf("[Captcha] debug_info primary pattern missed; used windowed fallback (script format drift)")
|
||||
}
|
||||
captchaDebugCache.Store(scriptURL, v)
|
||||
s.logger().Debugf("[Captcha] debug_info fetched url=%s", scriptURL)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// warnVersionDrift предупреждает один раз на версию, если VK отдал скрипт новее
|
||||
// оттестированного baseline: wire мог измениться, надо перепроверить против live.
|
||||
func (s *captchaSession) warnVersionDrift(scriptURL string) {
|
||||
m := reCaptchaVersion.FindStringSubmatch(scriptURL)
|
||||
if len(m) < 2 || m[1] == "" || m[1] == captchaScriptVersion {
|
||||
return
|
||||
}
|
||||
if _, seen := captchaVersionSeen.LoadOrStore(m[1], struct{}{}); seen {
|
||||
return
|
||||
}
|
||||
s.logger().Warnf("[Captcha] script version %s differs from tested %s; wire unverified - re-check if BOT-rejections rise", m[1], captchaScriptVersion)
|
||||
}
|
||||
|
||||
// extractDebugInfo достаёт fallback-константу debug_info из тела скрипта.
|
||||
// VK-формат: `debug_info:(...window.vk.X)||"<64hex>"`; имя window.vk.X плавает
|
||||
// от билда к билду, поэтому основной паттерн ловит только hash-литерал.
|
||||
// usedFallback=true - основной паттерн промазал, взят первый 64-hex в окне после
|
||||
// `debug_info` (страховка смены обёртки). Иначе - ошибка со снапшотом окна.
|
||||
func extractDebugInfo(body []byte) (value string, usedFallback bool, err error) {
|
||||
if m := reCaptchaDebugInfo.FindSubmatch(body); len(m) >= 2 {
|
||||
return string(m[1]), false, nil
|
||||
}
|
||||
idx := bytes.Index(body, []byte("debug_info"))
|
||||
if idx < 0 {
|
||||
return "", false, errors.New("debug_info marker not found in script")
|
||||
}
|
||||
end := idx + 256
|
||||
if end > len(body) {
|
||||
end = len(body)
|
||||
}
|
||||
if m := reCaptchaHex64.FindSubmatch(body[idx:end]); len(m) >= 2 {
|
||||
return string(m[1]), true, nil
|
||||
}
|
||||
return "", false, fmt.Errorf("debug_info match not found near: %q", body[idx:end])
|
||||
}
|
||||
|
||||
func parseCaptchaPage(html string) (*captchaPage, error) {
|
||||
page := &captchaPage{}
|
||||
|
||||
match := reCaptchaWindowInit.FindStringSubmatch(html)
|
||||
if len(match) < 2 {
|
||||
return nil, errors.New("captcha init json not found")
|
||||
}
|
||||
var init captchaInit
|
||||
if err := json.Unmarshal([]byte(match[1]), &init); err != nil {
|
||||
return nil, fmt.Errorf("captcha init json parse: %w", err)
|
||||
}
|
||||
page.Init = &init
|
||||
|
||||
match = reCaptchaScriptSrc.FindStringSubmatch(html)
|
||||
if len(match) < 2 {
|
||||
return nil, errors.New("captcha script url not found")
|
||||
}
|
||||
page.ScriptURL = match[1]
|
||||
|
||||
if m := reCaptchaPowInput.FindStringSubmatch(html); len(m) >= 2 {
|
||||
page.PowInput = m[1]
|
||||
}
|
||||
if page.PowInput == "" {
|
||||
return page, nil
|
||||
if m := reCaptchaDebugUUID.FindStringSubmatch(html); len(m) >= 2 {
|
||||
page.DebugInfo = m[1]
|
||||
}
|
||||
|
||||
match = reCaptchaDifficulty.FindStringSubmatch(html)
|
||||
if len(match) < 2 {
|
||||
m := reCaptchaPowInput.FindStringSubmatch(html)
|
||||
if len(m) < 2 {
|
||||
return nil, errors.New("captcha powInput not found")
|
||||
}
|
||||
page.PowInput = m[1]
|
||||
|
||||
dm := reCaptchaDifficulty.FindStringSubmatch(html)
|
||||
if len(dm) < 2 {
|
||||
return nil, errors.New("captcha difficulty const not found")
|
||||
}
|
||||
difficulty, err := strconv.Atoi(match[1])
|
||||
difficulty, err := strconv.Atoi(dm[1])
|
||||
if err != nil || difficulty <= 0 {
|
||||
return nil, fmt.Errorf("invalid captcha difficulty %q", match[1])
|
||||
return nil, fmt.Errorf("invalid captcha difficulty %q", dm[1])
|
||||
}
|
||||
page.PowDifficulty = difficulty
|
||||
return page, nil
|
||||
@@ -572,26 +507,12 @@ func solveCaptchaPoW(ctx context.Context, input string, difficulty int) string {
|
||||
sum := sha256.Sum256(buf)
|
||||
hashHex := hex.EncodeToString(sum[:])
|
||||
if strings.HasPrefix(hashHex, target) {
|
||||
return encodeCaptchaPoW(hashHex, nonce)
|
||||
return hashHex
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func encodeCaptchaPoW(hash string, nonce int) string {
|
||||
payload, err := json.Marshal(struct {
|
||||
Hash string `json:"hash"`
|
||||
Nonce int `json:"nonce"`
|
||||
}{
|
||||
Hash: hash,
|
||||
Nonce: nonce,
|
||||
})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "v2." + base64.RawURLEncoding.EncodeToString(payload)
|
||||
}
|
||||
|
||||
func (s *captchaSession) doRaw(
|
||||
method string,
|
||||
endpoint string,
|
||||
@@ -638,31 +559,6 @@ func (s *captchaSession) doRaw(
|
||||
return data, readErr
|
||||
}
|
||||
|
||||
func (s *captchaSession) logCaptchaPage(page *captchaPage) {
|
||||
showType := ""
|
||||
settingsSummary := "none"
|
||||
if page.Init != nil {
|
||||
showType = page.Init.Data.ShowCaptchaType
|
||||
parts := make([]string, 0, len(page.Init.Data.CaptchaSettings))
|
||||
for _, setting := range page.Init.Data.CaptchaSettings {
|
||||
ref := setting.contentRef()
|
||||
valueLen := 0
|
||||
if ref.Value != "" {
|
||||
valueLen = len(ref.Value)
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s:%s:%d", setting.Type, ref.Source, valueLen))
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
settingsSummary = strings.Join(parts, ",")
|
||||
}
|
||||
}
|
||||
scriptVersion := ""
|
||||
if m := reCaptchaVersion.FindStringSubmatch(page.ScriptURL); len(m) > 1 {
|
||||
scriptVersion = m[1]
|
||||
}
|
||||
s.logger().Debugf("[Captcha] page parsed show_type=%q settings=%s pow_difficulty=%d script_version=%q script_url=%s", showType, settingsSummary, page.PowDifficulty, scriptVersion, captchaSafeURL(page.ScriptURL))
|
||||
}
|
||||
|
||||
func captchaAPIResponseSummary(raw map[string]any) string {
|
||||
if errData, ok := raw["error"].(map[string]any); ok {
|
||||
return fmt.Sprintf("error code=%s msg=%q keys=%s", captchaStringifyAny(errData["error_code"]), captchaStringifyAny(errData["error_msg"]), captchaMapKeys(errData))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -34,99 +34,73 @@ func TestCaptchaInitSettingContentRefLegacySettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCaptchaPageSettingsKey(t *testing.T) {
|
||||
html := `<html><head><script src="https://static.vk.ru/vkid/1.1.1359/not_robot_captcha.js"></script></head><body>
|
||||
<script>
|
||||
window.init = {"data":{"show_captcha_type":"slider","captcha_settings":[{"type":"slider","settings_key":"abc123"}]}};
|
||||
const powInput = "input";
|
||||
func TestParseCaptchaPageSPA(t *testing.T) {
|
||||
html := `<html><head><script>
|
||||
const powInput = "Pihj7tyAHFxdwm4t";
|
||||
const difficulty = 2;
|
||||
</script>
|
||||
</script></head><body><div id="spa_root"></div>
|
||||
<script>window.vk = { statsMeta: {"hash":"39RzeEGG2Qq7t4YUwG"}, brlefapmjnpg: "1d958dbe-0663-42f2-95e5-0e2780ed2f93", id: 0 };</script>
|
||||
</body></html>`
|
||||
|
||||
page, err := parseCaptchaPage(html)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if page.Init == nil || len(page.Init.Data.CaptchaSettings) != 1 {
|
||||
t.Fatalf("captcha settings missing: %+v", page.Init)
|
||||
if page.PowInput != "Pihj7tyAHFxdwm4t" || page.PowDifficulty != 2 {
|
||||
t.Fatalf("pow parse = %q/%d", page.PowInput, page.PowDifficulty)
|
||||
}
|
||||
got := page.Init.Data.CaptchaSettings[0].contentRef()
|
||||
if got.Source != "settings_key" || got.Value != "abc123" {
|
||||
t.Fatalf("contentRef = %+v, want settings_key/abc123", got)
|
||||
if page.DebugInfo != "1d958dbe-0663-42f2-95e5-0e2780ed2f93" {
|
||||
t.Fatalf("debug_info = %q, want brlefapmjnpg uuid", page.DebugInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeCaptchaPoW(t *testing.T) {
|
||||
got := encodeCaptchaPoW("00abcdef", 147)
|
||||
if !strings.HasPrefix(got, "v2.") {
|
||||
t.Fatalf("pow = %q, want v2 prefix", got)
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(got, "v2."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded struct {
|
||||
Hash string `json:"hash"`
|
||||
Nonce int `json:"nonce"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded.Hash != "00abcdef" || decoded.Nonce != 147 {
|
||||
t.Fatalf("decoded = %+v, want hash/nonce", decoded)
|
||||
func TestParseCaptchaPageMissingPoW(t *testing.T) {
|
||||
if _, err := parseCaptchaPage(`<html><body><div id="spa_root"></div></body></html>`); err == nil {
|
||||
t.Fatal("expected error when powInput/difficulty absent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDebugInfo(t *testing.T) {
|
||||
const hash = "59f60d917b13be6a22c076adb2c17df37302c5314d8353a27e72f9fbcc9b4838"
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
wantFallback bool
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
name: "primary vk format",
|
||||
body: `x,debug_info:(null===(r=window.vk)||void 0===r?void 0:r.brlefapmjnpg)||"` + hash + `"});var a`,
|
||||
want: hash,
|
||||
},
|
||||
{
|
||||
name: "windowed fallback no or-wrapper",
|
||||
body: `debug_info: someRuntimeCall(),foo:"` + hash + `"`,
|
||||
want: hash,
|
||||
wantFallback: true,
|
||||
},
|
||||
{
|
||||
name: "no hash near marker",
|
||||
body: `debug_info:window.vk.x||"tooshort"`,
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "no marker",
|
||||
body: `nothing here`,
|
||||
fail: true,
|
||||
},
|
||||
func TestSolveCaptchaPoWRawHex(t *testing.T) {
|
||||
got := solveCaptchaPoW(context.Background(), "input", 1)
|
||||
if len(got) != 64 {
|
||||
t.Fatalf("pow = %q, want 64-hex", got)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, usedFallback, err := extractDebugInfo([]byte(tt.body))
|
||||
if tt.fail {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got %q", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("extractDebugInfo = %q, want %q", got, tt.want)
|
||||
}
|
||||
if usedFallback != tt.wantFallback {
|
||||
t.Fatalf("usedFallback = %v, want %v", usedFallback, tt.wantFallback)
|
||||
}
|
||||
})
|
||||
if !strings.HasPrefix(got, "0") {
|
||||
t.Fatalf("pow = %q, want leading zero for difficulty 1", got)
|
||||
}
|
||||
if again := solveCaptchaPoW(context.Background(), "input", 1); again != got {
|
||||
t.Fatalf("pow not deterministic: %q vs %q", got, again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCaptchaInitSession(t *testing.T) {
|
||||
raw := map[string]any{"response": map[string]any{
|
||||
"show_captcha_type": "slider",
|
||||
"captcha_id": "cid",
|
||||
"content_settings": []any{
|
||||
map[string]any{"type": "slider", "settings_key": "sliderkey"},
|
||||
map[string]any{"type": "sound", "settings_key": "soundkey"},
|
||||
},
|
||||
}}
|
||||
showType, content := parseCaptchaInitSession(raw)
|
||||
if showType != "slider" {
|
||||
t.Fatalf("show_type = %q, want slider", showType)
|
||||
}
|
||||
if content.Value != "sliderkey" || content.Source != "settings_key" {
|
||||
t.Fatalf("content = %+v, want sliderkey/settings_key", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCaptchaInitSessionCheckbox(t *testing.T) {
|
||||
raw := map[string]any{"response": map[string]any{
|
||||
"show_captcha_type": "checkbox",
|
||||
"content_settings": []any{
|
||||
map[string]any{"type": "slider", "settings_key": "k"},
|
||||
},
|
||||
}}
|
||||
showType, _ := parseCaptchaInitSession(raw)
|
||||
if showType != "checkbox" {
|
||||
t.Fatalf("show_type = %q, want checkbox", showType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user