You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1039 lines
28 KiB

package clientcore
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/color"
_ "image/jpeg" // register JPEG decoder
"io"
"log"
"math/rand"
neturl "net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
fhttp "github.com/bogdanfinn/fhttp"
tlsclient "github.com/bogdanfinn/tls-client"
)
const (
sliderCaptchaType = "slider"
defaultSliderAttempts = 4
)
type captchaNotRobotSession struct {
ctx context.Context
sessionToken string
hash string
streamID int
client tlsclient.HttpClient
profile Profile
browserFp string
adFp string
savedProfile *SavedProfile
}
func generateAdFp() string {
b := make([]byte, 16)
// simple random bytes (or any pseudo-random logic that matches the 21-char base64 footprint)
for i := range b {
b[i] = byte(rand.Intn(256))
}
return base64.RawURLEncoding.EncodeToString(b)[:21]
}
type captchaSettingsResponse struct {
ShowCaptchaType string
SettingsByType map[string]string
}
type captchaCheckResult struct {
Status string
SuccessToken string
ShowCaptchaType string
}
type sliderCaptchaContent struct {
Image image.Image
Size int
Steps []int
Attempts int
}
type sliderCandidate struct {
Index int
ActiveSteps []int
Score int64
}
type captchaBootstrap struct {
PowInput string
Difficulty int
Settings *captchaSettingsResponse
}
func newCaptchaNotRobotSession(ctx context.Context, sessionToken, hash string, streamID int, client tlsclient.HttpClient, profile Profile, savedProfile *SavedProfile) *captchaNotRobotSession {
browserFp := generateBrowserFp(profile)
if savedProfile != nil {
browserFp = savedProfile.BrowserFp
}
return &captchaNotRobotSession{
ctx: ctx,
sessionToken: sessionToken,
hash: hash,
streamID: streamID,
client: client,
profile: profile,
browserFp: browserFp,
adFp: generateAdFp(),
savedProfile: savedProfile,
}
}
func (s *captchaNotRobotSession) baseValues() neturl.Values {
values := neturl.Values{}
values.Set("session_token", s.sessionToken)
values.Set("domain", "vk.com")
values.Set("adFp", s.adFp)
values.Set("access_token", "")
return values
}
func (s *captchaNotRobotSession) request(method string, values neturl.Values) (map[string]interface{}, error) {
reqURL := "https://api.vk.ru/method/" + method + "?v=5.131"
req, err := fhttp.NewRequestWithContext(s.ctx, "POST", reqURL, strings.NewReader(values.Encode()))
if err != nil {
return nil, err
}
// Match the headers that the real VK captcha JS sends, same as callCaptchaNotRobot.
applyBrowserProfileFhttp(req, s.profile)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "https://api.vk.ru")
req.Header.Set("Referer", fmt.Sprintf("https://api.vk.ru/not_robot_captcha?domain=vk.com&session_token=%s&variant=popup&blank=1", s.sessionToken))
req.Header.Set("Sec-Fetch-Site", "same-origin")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
httpResp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = httpResp.Body.Close()
}()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
var resp map[string]interface{}
if err := json.Unmarshal(body, &resp); err != nil {
return nil, err
}
return resp, nil
}
func (s *captchaNotRobotSession) requestSettings() (*captchaSettingsResponse, error) {
resp, err := s.request("captchaNotRobot.settings", s.baseValues())
if err != nil {
return nil, fmt.Errorf("settings failed: %w", err)
}
return parseCaptchaSettingsResponse(resp)
}
func (s *captchaNotRobotSession) requestComponentDone() error {
values := s.baseValues()
values.Set("browser_fp", s.browserFp)
deviceJSON := buildCaptchaDeviceJSON(s.profile)
if s.savedProfile != nil {
deviceJSON = s.savedProfile.DeviceJSON
}
values.Set("device", deviceJSON)
resp, err := s.request("captchaNotRobot.componentDone", values)
if err != nil {
return fmt.Errorf("componentDone failed: %w", err)
}
respObj, ok := resp["response"].(map[string]interface{})
if ok {
if statusVal, ok := respObj["status"].(string); ok && statusVal != "" && statusVal != "OK" {
return fmt.Errorf("componentDone status: %s", statusVal)
}
}
return nil
}
func (s *captchaNotRobotSession) requestCheckboxCheck() (*captchaCheckResult, error) {
return s.requestCheck("[]", base64.StdEncoding.EncodeToString([]byte("{}")))
}
func (s *captchaNotRobotSession) requestSliderContent(sliderSettings string) (*sliderCaptchaContent, error) {
values := s.baseValues()
if sliderSettings != "" {
values.Set("captcha_settings", sliderSettings)
}
resp, err := s.request("captchaNotRobot.getContent", values)
if err != nil {
return nil, fmt.Errorf("getContent failed: %w", err)
}
return parseSliderCaptchaContentResponse(resp)
}
// requestSliderContentWithFallback tries to get slider content using multiple strategies:
// first with the provided captcha_settings, then without it (and vice versa).
// VK sometimes reports show_type=checkbox in settings but actually serves slider content,
// so we need to probe both variants.
func (s *captchaNotRobotSession) requestSliderContentWithFallback(sliderSettings string, streamID int) (*sliderCaptchaContent, error) {
type attempt struct {
settings string
desc string
}
var attempts []attempt
if sliderSettings != "" {
attempts = []attempt{
{settings: sliderSettings, desc: "with captcha_settings"},
{settings: "", desc: "without captcha_settings"},
}
} else {
// We have no slider settings; just one attempt without captcha_settings
attempts = []attempt{
{settings: "", desc: "without captcha_settings"},
}
}
var lastErr error
for _, a := range attempts {
log.Printf("[STREAM %d] [Captcha] Requesting slider content (%s)...", streamID, a.desc)
content, err := s.requestSliderContent(a.settings)
if err == nil {
return content, nil
}
log.Printf("[STREAM %d] [Captcha] getContent failed (%s): %v", streamID, a.desc, err)
lastErr = err
}
return nil, lastErr
}
func (s *captchaNotRobotSession) requestSliderCheck(activeSteps []int, candidateIndex int, candidateCount int) (*captchaCheckResult, error) {
answer, err := encodeSliderAnswer(activeSteps)
if err != nil {
return nil, err
}
return s.requestCheck("[]", answer)
}
func (s *captchaNotRobotSession) requestCheck(cursor string, answer string) (*captchaCheckResult, error) {
values := s.baseValues()
// The real browser sends a static SHA-256 hash for debug_info.
// We use the exact one captured from the real browser's session.
debugInfo := "f3ef768dab7a20f574c6461f34e4257894d2a3c30a53d8727a3edaf7ab70847d"
values.Set("accelerometer", "[]")
values.Set("gyroscope", "[]")
values.Set("motion", "[]")
values.Set("cursor", cursor)
values.Set("taps", "[]")
values.Set("connectionRtt", "[250,250,250,250,250]")
values.Set("connectionDownlink", "[1.45,1.45,1.45,1.45,1.45]")
values.Set("browser_fp", s.browserFp)
values.Set("hash", s.hash)
values.Set("answer", answer)
values.Set("debug_info", debugInfo)
resp, err := s.request("captchaNotRobot.check", values)
if err != nil {
return nil, fmt.Errorf("check failed: %w", err)
}
return parseCaptchaCheckResult(resp)
}
func (s *captchaNotRobotSession) requestEndSession() {
log.Printf("[STREAM %d] [Captcha] Step 4/4: endSession", s.streamID)
if _, err := s.request("captchaNotRobot.endSession", s.baseValues()); err != nil {
log.Printf("[STREAM %d] [Captcha] Warning: endSession failed: %v", s.streamID, err)
}
}
func callCaptchaNotRobotWithSliderPOC(
ctx context.Context,
sessionToken string,
hash string,
streamID int,
client tlsclient.HttpClient,
profile Profile,
initialSettings *captchaSettingsResponse,
savedProfile *SavedProfile,
) (string, error) {
session := newCaptchaNotRobotSession(ctx, sessionToken, hash, streamID, client, profile, savedProfile)
log.Printf("[STREAM %d] [Captcha] Step 1/4: settings", streamID)
settingsResp, err := session.requestSettings()
if err != nil {
return "", err
}
settingsResp = mergeCaptchaSettings(settingsResp, initialSettings)
time.Sleep(200 * time.Millisecond)
log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID)
err = session.requestComponentDone()
if err != nil {
return "", err
}
time.Sleep(200 * time.Millisecond)
log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID)
initialCheck, err := session.requestCheckboxCheck()
if err != nil {
return "", err
}
if initialCheck.Status == "OK" {
if initialCheck.SuccessToken == "" {
return "", fmt.Errorf("success_token not found")
}
session.requestEndSession()
return initialCheck.SuccessToken, nil
}
sliderSettings, hasSlider := settingsResp.SettingsByType[sliderCaptchaType]
log.Printf(
"[STREAM %d] [Captcha] Checkbox-style check returned status=%s (settings show_type=%q, check show_type=%q, available_types=%s)",
streamID,
initialCheck.Status,
settingsResp.ShowCaptchaType,
initialCheck.ShowCaptchaType,
describeCaptchaTypes(settingsResp.SettingsByType),
)
if !hasSlider {
log.Printf(
"[STREAM %d] [Captcha] Slider settings not found in settings response. Trying getContent without captcha_settings...",
streamID,
)
} else {
log.Printf("[STREAM %d] [Captcha] Trying experimental slider solver...", streamID)
}
// After check returns BOT, a real browser renders the slider widget and calls
// componentDone again to signal "slider component is now loaded". Without this,
// VK refuses getContent with ERROR because it expects the widget lifecycle.
log.Printf("[STREAM %d] [Captcha] Re-registering slider component before getContent...", streamID)
time.Sleep(300 * time.Millisecond)
err = session.requestComponentDone()
if err != nil {
// Non-fatal: log and continue — getContent may still succeed.
log.Printf("[STREAM %d] [Captcha] Warning: slider componentDone failed: %v", streamID, err)
}
time.Sleep(200 * time.Millisecond)
sliderContent, err := session.requestSliderContentWithFallback(sliderSettings, streamID)
if err != nil {
log.Printf(
"[STREAM %d] [Captcha] All slider getContent attempts failed: %v",
streamID,
err,
)
return "", fmt.Errorf("check status: %s (slider getContent failed: %w)", initialCheck.Status, err)
}
candidates, err := rankSliderCandidates(sliderContent.Image, sliderContent.Size, sliderContent.Steps)
if err != nil {
return "", err
}
log.Printf(
"[STREAM %d] [Captcha] Ranked %d slider positions locally; submitting top %d based on attempt budget %d",
streamID,
len(candidates),
minInt(sliderContent.Attempts, len(candidates)),
sliderContent.Attempts,
)
successToken, err := trySliderCaptchaCandidates(candidates, sliderContent.Attempts, func(candidate sliderCandidate) (*captchaCheckResult, error) {
log.Printf(
"[STREAM %d] [Captcha] Slider guess position=%d score=%d",
streamID,
candidate.Index,
candidate.Score,
)
return session.requestSliderCheck(candidate.ActiveSteps, candidate.Index, len(candidates))
})
if err != nil {
return "", err
}
session.requestEndSession()
return successToken, nil
}
func buildCaptchaDeviceJSON(profile Profile) string {
// Fallback device JSON if no saved profile is available.
// We include the User-Agent from the current profile to maintain some consistency.
return fmt.Sprintf(
`{"screenWidth":1536,"screenHeight":864,"screenAvailWidth":1536,"screenAvailHeight":816,"innerWidth":1536,"innerHeight":730,"devicePixelRatio":1.25,"language":"ru-RU","languages":["ru-RU","ru","en-US","en"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"prompt","userAgent":"%s"}`,
profile.UserAgent,
)
}
func parseCaptchaSettingsResponse(resp map[string]interface{}) (*captchaSettingsResponse, error) {
respObj, ok := resp["response"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid settings response: %v", resp)
}
settings := &captchaSettingsResponse{
SettingsByType: make(map[string]string),
}
settings.ShowCaptchaType, _ = respObj["show_captcha_type"].(string) //nolint:errcheck
rawSettings, ok := expandCaptchaSettings(respObj["captcha_settings"])
if !ok {
return settings, nil
}
for _, rawItem := range rawSettings {
item, ok := rawItem.(map[string]interface{})
if !ok {
continue
}
captchaType, _ := item["type"].(string) //nolint:errcheck
if captchaType == "" {
continue
}
normalized, err := normalizeCaptchaSettings(item["settings"])
if err != nil {
return nil, fmt.Errorf("invalid captcha_settings for %s: %w", captchaType, err)
}
settings.SettingsByType[captchaType] = normalized
}
return settings, nil
}
func parseCaptchaBootstrapHTML(html string) (*captchaBootstrap, error) {
powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`)
powInputMatch := powInputRe.FindStringSubmatch(html)
if len(powInputMatch) < 2 {
return nil, fmt.Errorf("powInput not found in captcha HTML")
}
difficulty := 2
for _, expr := range []*regexp.Regexp{
regexp.MustCompile(`startsWith\('0'\.repeat\((\d+)\)\)`),
regexp.MustCompile(`const\s+difficulty\s*=\s*(\d+)`),
} {
if match := expr.FindStringSubmatch(html); len(match) >= 2 {
if parsed, err := strconv.Atoi(match[1]); err == nil {
difficulty = parsed
break
}
}
}
settings, err := parseCaptchaSettingsFromHTML(html)
if err != nil {
return nil, err
}
return &captchaBootstrap{
PowInput: powInputMatch[1],
Difficulty: difficulty,
Settings: settings,
}, nil
}
func parseCaptchaSettingsFromHTML(html string) (*captchaSettingsResponse, error) {
initRe := regexp.MustCompile(`(?s)window\.init\s*=\s*(\{.*?})\s*;\s*window\.lang`)
initMatch := initRe.FindStringSubmatch(html)
if len(initMatch) < 2 {
return &captchaSettingsResponse{SettingsByType: make(map[string]string)}, nil
}
var initPayload struct {
Data struct {
ShowCaptchaType string `json:"show_captcha_type"`
CaptchaSettings interface{} `json:"captcha_settings"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(initMatch[1]), &initPayload); err != nil {
return nil, fmt.Errorf("parse window.init captcha data: %w", err)
}
return parseCaptchaSettingsResponse(map[string]interface{}{
"response": map[string]interface{}{
"show_captcha_type": initPayload.Data.ShowCaptchaType,
"captcha_settings": initPayload.Data.CaptchaSettings,
},
})
}
func mergeCaptchaSettings(primary *captchaSettingsResponse, fallback *captchaSettingsResponse) *captchaSettingsResponse {
if primary == nil {
return cloneCaptchaSettings(fallback)
}
if primary.SettingsByType == nil {
primary.SettingsByType = make(map[string]string)
}
if fallback == nil {
return primary
}
if primary.ShowCaptchaType == "" {
primary.ShowCaptchaType = fallback.ShowCaptchaType
}
for captchaType, settings := range fallback.SettingsByType {
if _, exists := primary.SettingsByType[captchaType]; !exists {
primary.SettingsByType[captchaType] = settings
}
}
return primary
}
func cloneCaptchaSettings(src *captchaSettingsResponse) *captchaSettingsResponse {
if src == nil {
return nil
}
cloned := &captchaSettingsResponse{
ShowCaptchaType: src.ShowCaptchaType,
SettingsByType: make(map[string]string, len(src.SettingsByType)),
}
for captchaType, settings := range src.SettingsByType {
cloned.SettingsByType[captchaType] = settings
}
return cloned
}
func expandCaptchaSettings(raw interface{}) ([]interface{}, bool) {
switch value := raw.(type) {
case nil:
return nil, false
case []interface{}:
return value, true
case map[string]interface{}:
items := make([]interface{}, 0, len(value))
for captchaType, settings := range value {
items = append(items, map[string]interface{}{
"type": captchaType,
"settings": settings,
})
}
return items, true
case string:
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil, false
}
var items []interface{}
if err := json.Unmarshal([]byte(trimmed), &items); err == nil {
return items, true
}
var mapping map[string]interface{}
if err := json.Unmarshal([]byte(trimmed), &mapping); err == nil {
return expandCaptchaSettings(mapping)
}
}
return nil, false
}
func normalizeCaptchaSettings(raw interface{}) (string, error) {
switch value := raw.(type) {
case nil:
return "", nil
case string:
return value, nil
default:
data, err := json.Marshal(value)
if err != nil {
return "", err
}
return string(data), nil
}
}
func parseCaptchaCheckResult(resp map[string]interface{}) (*captchaCheckResult, error) {
respObj, ok := resp["response"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid check response: %v", resp)
}
result := &captchaCheckResult{}
result.Status, _ = respObj["status"].(string) //nolint:errcheck
result.SuccessToken, _ = respObj["success_token"].(string) //nolint:errcheck
result.ShowCaptchaType, _ = respObj["show_captcha_type"].(string) //nolint:errcheck
if result.Status == "" {
return nil, fmt.Errorf("check status missing: %v", resp)
}
return result, nil
}
func parseSliderCaptchaContentResponse(resp map[string]interface{}) (*sliderCaptchaContent, error) {
respObj, ok := resp["response"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid slider content response: %v", resp)
}
status, _ := respObj["status"].(string) //nolint:errcheck
if status != "OK" {
// Log all fields from the response to help diagnose why VK rejected getContent.
var debugFields []string
for k, v := range respObj {
if k != "image" { // skip base64 image blob
debugFields = append(debugFields, fmt.Sprintf("%s=%v", k, v))
}
}
sort.Strings(debugFields)
log.Printf("[Captcha] getContent ERROR response fields: %s", strings.Join(debugFields, " "))
return nil, fmt.Errorf("slider getContent status: %s", status)
}
extension, _ := respObj["extension"].(string) //nolint:errcheck
extension = strings.ToLower(extension)
if extension != "jpeg" && extension != "jpg" {
return nil, fmt.Errorf("unsupported slider image format: %s", extension)
}
rawImage, _ := respObj["image"].(string) //nolint:errcheck
if rawImage == "" {
return nil, fmt.Errorf("slider image missing")
}
rawSteps, ok := respObj["steps"].([]interface{})
if !ok {
return nil, fmt.Errorf("slider steps missing")
}
steps, err := parseIntSlice(rawSteps)
if err != nil {
return nil, err
}
size, swaps, attempts, err := parseSliderSteps(steps)
if err != nil {
return nil, err
}
img, err := decodeSliderImage(rawImage)
if err != nil {
return nil, err
}
return &sliderCaptchaContent{
Image: img,
Size: size,
Steps: swaps,
Attempts: attempts,
}, nil
}
func parseIntSlice(raw []interface{}) ([]int, error) {
values := make([]int, 0, len(raw))
for _, item := range raw {
number, err := parseIntValue(item)
if err != nil {
return nil, err
}
values = append(values, number)
}
return values, nil
}
func parseIntValue(raw interface{}) (int, error) {
switch value := raw.(type) {
case float64:
return int(value), nil
case int:
return value, nil
case string:
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return 0, fmt.Errorf("invalid numeric value: %v", raw)
}
return parsed, nil
default:
return 0, fmt.Errorf("invalid numeric value: %v", raw)
}
}
func parseSliderSteps(steps []int) (int, []int, int, error) {
if len(steps) < 3 {
return 0, nil, 0, fmt.Errorf("slider steps payload too short")
}
size := steps[0]
if size <= 0 {
return 0, nil, 0, fmt.Errorf("invalid slider size: %d", size)
}
remaining := append([]int(nil), steps[1:]...)
attempts := defaultSliderAttempts
if len(remaining)%2 != 0 {
attempts = remaining[len(remaining)-1]
remaining = remaining[:len(remaining)-1]
}
if attempts <= 0 {
attempts = defaultSliderAttempts
}
if len(remaining) == 0 || len(remaining)%2 != 0 {
return 0, nil, 0, fmt.Errorf("invalid slider swap payload")
}
return size, remaining, attempts, nil
}
func decodeSliderImage(rawImage string) (image.Image, error) {
decoded, err := base64.StdEncoding.DecodeString(rawImage)
if err != nil {
return nil, fmt.Errorf("decode slider image: %w", err)
}
img, _, err := image.Decode(bytes.NewReader(decoded))
if err != nil {
return nil, fmt.Errorf("decode slider image: %w", err)
}
return img, nil
}
func encodeSliderAnswer(activeSteps []int) (string, error) {
payload := struct {
Value []int `json:"value"`
}{
Value: activeSteps,
}
data, err := json.Marshal(payload)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(data), nil
}
func buildSliderActiveSteps(swaps []int, candidateIndex int) []int {
if candidateIndex <= 0 {
return []int{}
}
end := candidateIndex * 2
if end > len(swaps) {
end = len(swaps)
}
return append([]int(nil), swaps[:end]...)
}
func buildSliderTileMapping(gridSize int, activeSteps []int) ([]int, error) {
tileCount := gridSize * gridSize
if tileCount <= 0 {
return nil, fmt.Errorf("invalid slider tile count: %d", tileCount)
}
if len(activeSteps)%2 != 0 {
return nil, fmt.Errorf("invalid active steps length: %d", len(activeSteps))
}
mapping := make([]int, tileCount)
for i := range mapping {
mapping[i] = i
}
for idx := 0; idx < len(activeSteps); idx += 2 {
left := activeSteps[idx]
right := activeSteps[idx+1]
if left < 0 || right < 0 || left >= tileCount || right >= tileCount {
return nil, fmt.Errorf("slider step out of range: %d,%d", left, right)
}
mapping[left], mapping[right] = mapping[right], mapping[left]
}
return mapping, nil
}
func rankSliderCandidates(img image.Image, gridSize int, swaps []int) ([]sliderCandidate, error) {
candidateCount := len(swaps) / 2
if candidateCount == 0 {
return nil, fmt.Errorf("slider has no candidates")
}
candidates := make([]sliderCandidate, 0, candidateCount)
for idx := 1; idx <= candidateCount; idx++ {
activeSteps := buildSliderActiveSteps(swaps, idx)
mapping, err := buildSliderTileMapping(gridSize, activeSteps)
if err != nil {
return nil, err
}
score, err := scoreSliderCandidate(img, gridSize, mapping)
if err != nil {
return nil, err
}
candidates = append(candidates, sliderCandidate{
Index: idx,
ActiveSteps: activeSteps,
Score: score,
})
}
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Score == candidates[j].Score {
return candidates[i].Index < candidates[j].Index
}
return candidates[i].Score < candidates[j].Score
})
return candidates, nil
}
func scoreSliderCandidate(img image.Image, gridSize int, mapping []int) (int64, error) {
bounds := img.Bounds()
var score int64
for row := 0; row < gridSize; row++ {
for col := 0; col < gridSize-1; col++ {
dstLeftIndex := row*gridSize + col
dstRightIndex := row*gridSize + col + 1
srcLeftIndex := mapping[dstLeftIndex]
srcRightIndex := mapping[dstRightIndex]
dstLeftRect := sliderTileRect(bounds, gridSize, dstLeftIndex)
dstRightRect := sliderTileRect(bounds, gridSize, dstRightIndex)
srcLeftRect := sliderTileRect(bounds, gridSize, srcLeftIndex)
srcRightRect := sliderTileRect(bounds, gridSize, srcRightIndex)
height := minInt(dstLeftRect.Dy(), dstRightRect.Dy())
leftSrcXRel := (dstLeftRect.Dx() - 1) * srcLeftRect.Dx() / dstLeftRect.Dx()
sxLeft := srcLeftRect.Min.X + leftSrcXRel
sxRight := srcRightRect.Min.X
for offset := 0; offset < height; offset++ {
syLeft := srcLeftRect.Min.Y + offset*srcLeftRect.Dy()/dstLeftRect.Dy()
syRight := srcRightRect.Min.Y + offset*srcRightRect.Dy()/dstRightRect.Dy()
score += pixelDiff(
img.At(sxLeft, syLeft),
img.At(sxRight, syRight),
)
}
}
}
for row := 0; row < gridSize-1; row++ {
for col := 0; col < gridSize; col++ {
dstTopIndex := row*gridSize + col
dstBottomIndex := (row+1)*gridSize + col
srcTopIndex := mapping[dstTopIndex]
srcBottomIndex := mapping[dstBottomIndex]
dstTopRect := sliderTileRect(bounds, gridSize, dstTopIndex)
dstBottomRect := sliderTileRect(bounds, gridSize, dstBottomIndex)
srcTopRect := sliderTileRect(bounds, gridSize, srcTopIndex)
srcBottomRect := sliderTileRect(bounds, gridSize, srcBottomIndex)
width := minInt(dstTopRect.Dx(), dstBottomRect.Dx())
topSrcYRel := (dstTopRect.Dy() - 1) * srcTopRect.Dy() / dstTopRect.Dy()
syTop := srcTopRect.Min.Y + topSrcYRel
syBottom := srcBottomRect.Min.Y
for offset := 0; offset < width; offset++ {
sxTop := srcTopRect.Min.X + offset*srcTopRect.Dx()/dstTopRect.Dx()
sxBottom := srcBottomRect.Min.X + offset*srcBottomRect.Dx()/dstBottomRect.Dx()
score += pixelDiff(
img.At(sxTop, syTop),
img.At(sxBottom, syBottom),
)
}
}
}
return score, nil
}
func renderSliderCandidate(img image.Image, gridSize int, mapping []int) (*image.RGBA, error) {
if gridSize <= 0 {
return nil, fmt.Errorf("invalid grid size: %d", gridSize)
}
tileCount := gridSize * gridSize
if len(mapping) != tileCount {
return nil, fmt.Errorf("unexpected tile mapping length: %d", len(mapping))
}
bounds := img.Bounds()
rendered := image.NewRGBA(bounds)
for dstIndex, srcIndex := range mapping {
srcRect := sliderTileRect(bounds, gridSize, srcIndex)
dstRect := sliderTileRect(bounds, gridSize, dstIndex)
copyScaledTile(rendered, dstRect, img, srcRect)
}
return rendered, nil
}
func sliderTileRect(bounds image.Rectangle, gridSize int, index int) image.Rectangle {
row := index / gridSize
col := index % gridSize
x0 := bounds.Min.X + col*bounds.Dx()/gridSize
x1 := bounds.Min.X + (col+1)*bounds.Dx()/gridSize
y0 := bounds.Min.Y + row*bounds.Dy()/gridSize
y1 := bounds.Min.Y + (row+1)*bounds.Dy()/gridSize
return image.Rect(x0, y0, x1, y1)
}
func copyScaledTile(dst *image.RGBA, dstRect image.Rectangle, src image.Image, srcRect image.Rectangle) {
if dstRect.Empty() || srcRect.Empty() {
return
}
dstWidth := dstRect.Dx()
dstHeight := dstRect.Dy()
srcWidth := srcRect.Dx()
srcHeight := srcRect.Dy()
for y := 0; y < dstHeight; y++ {
sy := srcRect.Min.Y + y*srcHeight/dstHeight
for x := 0; x < dstWidth; x++ {
sx := srcRect.Min.X + x*srcWidth/dstWidth
dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, src.At(sx, sy))
}
}
}
func pixelDiff(left color.Color, right color.Color) int64 {
lr, lg, lb, _ := left.RGBA()
rr, rg, rb, _ := right.RGBA()
return absDiff(lr, rr) + absDiff(lg, rg) + absDiff(lb, rb)
}
func absDiff(left uint32, right uint32) int64 {
if left > right {
return int64(left - right)
}
return int64(right - left)
}
/*
func generateSliderCursor(candidateIndex int, candidateCount int) string {
return buildSliderCursor(candidateIndex, candidateCount, time.Now().Add(-220*time.Millisecond).UnixMilli())
}
func buildSliderCursor(candidateIndex int, candidateCount int, startTime int64) string {
if candidateCount <= 0 {
return "[]"
}
type cursorPoint struct {
X int `json:"x"`
Y int `json:"y"`
T int64 `json:"t"`
}
startX := 140
endX := startX + 620*candidateIndex/candidateCount
startY := 430
points := make([]cursorPoint, 0, 12)
for step := 0; step < 12; step++ {
x := startX + (endX-startX)*step/11
y := startY + ((step % 3) - 1)
points = append(points, cursorPoint{
X: x,
Y: y,
T: startTime + int64(step*18),
})
}
data, err := json.Marshal(points)
if err != nil {
return "[]"
}
return string(data)
}
*/
func trySliderCaptchaCandidates(
candidates []sliderCandidate,
maxAttempts int,
check func(candidate sliderCandidate) (*captchaCheckResult, error),
) (string, error) {
if len(candidates) == 0 {
return "", fmt.Errorf("slider has no ranked candidates")
}
limit := minInt(maxAttempts, len(candidates))
if limit <= 0 {
return "", fmt.Errorf("slider has no attempts available")
}
for idx := 0; idx < limit; idx++ {
result, err := check(candidates[idx])
if err != nil {
return "", err
}
switch result.Status {
case "OK":
if result.SuccessToken == "" {
return "", fmt.Errorf("success_token not found")
}
return result.SuccessToken, nil
case "ERROR_LIMIT":
return "", fmt.Errorf("slider check status: %s", result.Status)
default:
continue
}
}
return "", fmt.Errorf("slider guesses exhausted")
}
func minInt(left int, right int) int {
if left < right {
return left
}
return right
}
func describeCaptchaTypes(settingsByType map[string]string) string {
if len(settingsByType) == 0 {
return "none"
}
types := make([]string, 0, len(settingsByType))
for captchaType := range settingsByType {
types = append(types, captchaType)
}
sort.Strings(types)
return strings.Join(types, ",")
}