Browse Source

Merge remote-tracking branch 'upstream/main'

pull/102/head
Moroka8 2 months ago
parent
commit
1d2fea2040
  1. 4
      README.md
  2. 182
      client/main.go
  3. 61
      client/main_test.go
  4. 37
      client/manual_captcha.go
  5. 65
      client/manual_captcha_test.go
  6. 918
      client/slider_captcha.go
  7. 278
      client/slider_captcha_test.go
  8. 27
      go.mod
  9. 68
      go.sum
  10. 22
      server/main.go

4
README.md

@ -295,6 +295,10 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down
Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 МБит/с для ВК)
Для прохождения капчи вручную - `-manual-captcha`.
По умолчанию капча теперь проходит так: обычная автопопытка, затем автопопытка через пазл-слайдер POC, и только потом ручной режим.
## Яндекс телемост
**UPD. ТЕЛЕМОСТ ЗАКРЫЛИ**

182
client/main.go

@ -8,7 +8,6 @@ import (
"context"
"crypto/md5"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
@ -22,7 +21,6 @@ import (
neturl "net/url"
"os"
"os/signal"
"regexp"
"strconv"
"strings"
"sync"
@ -68,8 +66,52 @@ var (
handshakeSem = make(chan struct{}, 3)
isDebug bool
manualCaptcha bool
autoCaptchaSliderPOC bool
)
type captchaSolveMode int
const (
captchaSolveModeAuto captchaSolveMode = iota
captchaSolveModeSliderPOC
captchaSolveModeManual
)
func captchaSolveModeForAttempt(attempt int, manualOnly bool, enableSliderPOC bool) (captchaSolveMode, bool) {
if manualOnly {
return captchaSolveModeManual, attempt == 0
}
switch attempt {
case 0:
return captchaSolveModeAuto, true
case 1:
if enableSliderPOC {
return captchaSolveModeSliderPOC, true
}
return captchaSolveModeManual, true
case 2:
if enableSliderPOC {
return captchaSolveModeManual, true
}
}
return 0, false
}
func captchaSolveModeLabel(mode captchaSolveMode) string {
switch mode {
case captchaSolveModeAuto:
return "auto captcha"
case captchaSolveModeSliderPOC:
return "auto captcha slider POC"
case captchaSolveModeManual:
return "manual captcha"
default:
return "captcha"
}
}
type UDPPacket struct {
Data []byte
N int
@ -337,8 +379,12 @@ func (e *VkCaptchaError) IsCaptchaError() bool {
return e.ErrorCode == 14 && e.RedirectURI != "" && e.SessionToken != ""
}
func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) {
log.Printf("[STREAM %d] [Captcha] Solving Not Robot Captcha...", streamID)
func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, profile Profile, useSliderPOC bool) (string, error) {
if useSliderPOC {
log.Printf("[STREAM %d] [Captcha] Solving captcha with slider POC...", streamID)
} else {
log.Printf("[STREAM %d] [Captcha] Solving captcha...", streamID)
}
if captchaErr.SessionToken == "" {
return "", fmt.Errorf("no session_token in redirect_uri for auto-solve")
@ -347,17 +393,30 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in
return "", fmt.Errorf("no redirect_uri for auto-solve")
}
powInput, difficulty, err := fetchPowInput(ctx, captchaErr.RedirectURI, client, profile)
bootstrap, err := fetchCaptchaBootstrap(ctx, captchaErr.RedirectURI, client, profile)
if err != nil {
return "", fmt.Errorf("failed to fetch PoW input: %w", err)
return "", fmt.Errorf("failed to fetch captcha bootstrap: %w", err)
}
log.Printf("[STREAM %d] [Captcha] PoW input: %s, difficulty: %d", streamID, powInput, difficulty)
log.Printf("[STREAM %d] [Captcha] PoW input: %s, difficulty: %d", streamID, bootstrap.PowInput, bootstrap.Difficulty)
hash := solvePoW(powInput, difficulty)
hash := solvePoW(bootstrap.PowInput, bootstrap.Difficulty)
log.Printf("[STREAM %d] [Captcha] PoW solved: hash=%s", streamID, hash)
successToken, err := callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, client, profile)
var successToken string
if useSliderPOC {
successToken, err = callCaptchaNotRobotWithSliderPOC(
ctx,
captchaErr.SessionToken,
hash,
streamID,
client,
profile,
bootstrap.Settings,
)
} else {
successToken, err = callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, client, profile)
}
if err != nil {
return "", fmt.Errorf("captchaNotRobot API failed: %w", err)
}
@ -366,16 +425,16 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in
return successToken, nil
}
func fetchPowInput(ctx context.Context, RedirectURI string, client tlsclient.HttpClient, profile Profile) (string, int, error) {
parsedURL, err := neturl.Parse(RedirectURI)
func fetchCaptchaBootstrap(ctx context.Context, redirectURI string, client tlsclient.HttpClient, profile Profile) (*captchaBootstrap, error) {
parsedURL, err := neturl.Parse(redirectURI)
if err != nil {
return "", 0, err
return nil, err
}
domain := parsedURL.Hostname()
req, err := fhttp.NewRequestWithContext(ctx, "GET", RedirectURI, nil)
if err != nil {
return "", 0, err
return nil, err
}
req.Host = domain
@ -387,7 +446,7 @@ func fetchPowInput(ctx context.Context, RedirectURI string, client tlsclient.Htt
resp, err := client.Do(req)
if err != nil {
return "", 0, err
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
@ -395,26 +454,9 @@ func fetchPowInput(ctx context.Context, RedirectURI string, client tlsclient.Htt
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", 0, err
}
html := string(body)
powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`)
powInputMatch := powInputRe.FindStringSubmatch(html)
if len(powInputMatch) < 2 {
return "", 0, fmt.Errorf("powInput not found in captcha HTML")
}
powInput := powInputMatch[1]
diffRe := regexp.MustCompile(`startsWith\('0'\.repeat\((\d+)\)\)`)
diffMatch := diffRe.FindStringSubmatch(html)
difficulty := 2
if len(diffMatch) >= 2 {
if d, err := strconv.Atoi(diffMatch[1]); err == nil {
difficulty = d
}
return nil, err
}
return powInput, difficulty, nil
return parseCaptchaBootstrapHTML(string(body))
}
func solvePoW(powInput string, difficulty int) string {
@ -486,7 +528,7 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI
log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID)
browserFp := generateBrowserFp(profile)
deviceJSON := fmt.Sprintf(`{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1040,"innerWidth":1920,"innerHeight":969,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"default","userAgent":"%s","platform":"Win32"}`, profile.UserAgent)
deviceJSON := buildCaptchaDeviceJSON(profile)
componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON))
if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil {
@ -865,11 +907,12 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
urlAddr := fmt.Sprintf("https://api.vk.ru/method/calls.getAnonymousToken?v=5.275&client_id=%s", creds.ClientID)
var token2 string
maxAutoAttempts := 2
if manualCaptcha {
maxAutoAttempts = 0
for attempt := 0; ; attempt++ {
solveMode, hasSolveMode := captchaSolveModeForAttempt(attempt, manualCaptcha, autoCaptchaSliderPOC)
if !hasSolveMode {
break
}
for attempt := 0; attempt <= maxAutoAttempts+1; attempt++ {
resp, err = doRequest(data, urlAddr)
if err != nil {
return "", "", "", err
@ -882,20 +925,27 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
var captchaKey string
var solveErr error
if attempt < maxAutoAttempts {
// Auto Solve Attempts
switch solveMode {
case captchaSolveModeAuto:
if captchaErr.SessionToken != "" && captchaErr.RedirectURI != "" {
successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile)
successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile, false)
if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr)
log.Printf("[STREAM %d] [Captcha] Auto captcha failed: %v", streamID, solveErr)
}
} else {
solveErr = fmt.Errorf("missing fields for auto solve")
}
} else if attempt == maxAutoAttempts {
// Manual Solve Fallback with 60s Timeout
log.Printf("[STREAM %d] [Captcha] Auto failed %d times. Triggering MANUAL fallback...", streamID, maxAutoAttempts)
case captchaSolveModeSliderPOC:
if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" {
successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile, true)
if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] Auto captcha slider POC failed: %v", streamID, solveErr)
}
} else {
solveErr = fmt.Errorf("missing fields for slider POC auto solve")
}
case captchaSolveModeManual:
log.Printf("[STREAM %d] [Captcha] Triggering manual captcha fallback...", streamID)
manualCtx, manualCancel := context.WithTimeout(ctx, 60*time.Second)
type manualRes struct {
@ -927,29 +977,15 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
solveErr = fmt.Errorf("manual captcha timed out after 60s")
}
manualCancel()
} else {
solveErr = fmt.Errorf("max attempts reached")
}
// If solving failed (auto or manual) or timed out
if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] Failed to solve (attempt %d): %v", streamID, attempt+1, solveErr)
log.Printf("[STREAM %d] [Captcha] %s failed (attempt %d): %v", streamID, captchaSolveModeLabel(solveMode), attempt+1, solveErr)
if attempt < maxAutoAttempts-1 {
log.Printf("[STREAM %d] [Captcha] Backing off for 10 seconds before next auto attempt...", streamID)
select {
case <-ctx.Done():
return "", "", "", ctx.Err()
case <-time.After(10 * time.Second):
}
continue
} else if attempt == maxAutoAttempts-1 {
log.Printf("[STREAM %d] [Captcha] Backing off for 2 seconds before manual fallback...", streamID)
select {
case <-ctx.Done():
return "", "", "", ctx.Err()
case <-time.After(2 * time.Second):
}
nextSolveMode, hasNextSolveMode := captchaSolveModeForAttempt(attempt+1, manualCaptcha, autoCaptchaSliderPOC)
if hasNextSolveMode {
log.Printf("[STREAM %d] [Captcha] Falling back to %s...", streamID, captchaSolveModeLabel(nextSolveMode))
continue
}
@ -1354,13 +1390,6 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.
if err != nil {
return nil, err
}
config := &dtls.Config{
Certificates: []tls.Certificate{certificate},
InsecureSkipVerify: true,
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
ConnectionIDGenerator: dtls.OnlySendCIDGenerator(),
}
select {
case handshakeSem <- struct{}{}:
@ -1371,7 +1400,15 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.
ctx1, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
dtlsConn, err := dtls.Client(conn, peer, config)
dtlsConn, err := dtls.ClientWithOptions(
conn,
peer,
dtls.WithCertificates(certificate),
dtls.WithInsecureSkipVerify(true),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
dtls.WithConnectionIDGenerator(dtls.OnlySendCIDGenerator()),
)
if err != nil {
return nil, err
}
@ -1778,6 +1815,7 @@ func main() {
isDebug = *debugFlag
manualCaptcha = *manualCaptchaFlag
autoCaptchaSliderPOC = !manualCaptcha
var link string
var getCreds getCredsFunc

61
client/main_test.go

@ -0,0 +1,61 @@
package main
import "testing"
func TestCaptchaSolveModeForAttempt(t *testing.T) {
t.Parallel()
t.Run("default flow", func(t *testing.T) {
t.Parallel()
mode, ok := captchaSolveModeForAttempt(0, false, true)
if !ok || mode != captchaSolveModeAuto {
t.Fatalf("expected first attempt to use auto captcha, got mode=%v ok=%v", mode, ok)
}
mode, ok = captchaSolveModeForAttempt(1, false, true)
if !ok || mode != captchaSolveModeSliderPOC {
t.Fatalf("expected second attempt to use slider POC, got mode=%v ok=%v", mode, ok)
}
mode, ok = captchaSolveModeForAttempt(2, false, true)
if !ok || mode != captchaSolveModeManual {
t.Fatalf("expected third attempt to use manual captcha, got mode=%v ok=%v", mode, ok)
}
if _, ok = captchaSolveModeForAttempt(3, false, true); ok {
t.Fatal("expected no fourth captcha attempt in default flow")
}
})
t.Run("manual only flow", func(t *testing.T) {
t.Parallel()
mode, ok := captchaSolveModeForAttempt(0, true, true)
if !ok || mode != captchaSolveModeManual {
t.Fatalf("expected manual mode on first attempt, got mode=%v ok=%v", mode, ok)
}
if _, ok = captchaSolveModeForAttempt(1, true, true); ok {
t.Fatal("expected only one manual captcha attempt when manual mode is forced")
}
})
t.Run("flow without slider poc", func(t *testing.T) {
t.Parallel()
mode, ok := captchaSolveModeForAttempt(0, false, false)
if !ok || mode != captchaSolveModeAuto {
t.Fatalf("expected auto captcha first, got mode=%v ok=%v", mode, ok)
}
mode, ok = captchaSolveModeForAttempt(1, false, false)
if !ok || mode != captchaSolveModeManual {
t.Fatalf("expected manual captcha second when slider POC is disabled, got mode=%v ok=%v", mode, ok)
}
if _, ok = captchaSolveModeForAttempt(2, false, false); ok {
t.Fatal("expected only two attempts when slider POC is disabled")
}
})
}

37
client/manual_captcha.go

@ -74,6 +74,32 @@ func targetOrigin(targetURL *neturl.URL) string {
return targetURL.Scheme + "://" + targetURL.Host
}
func isSafeLocalRedirectPath(raw string) bool {
if raw == "" || raw[0] != '/' {
return false
}
if len(raw) > 1 && (raw[1] == '/' || raw[1] == '\\') {
return false
}
return true
}
func rewriteProxyRedirectLocation(raw string, targetURL *neturl.URL) (string, bool) {
if isSafeLocalRedirectPath(raw) {
return raw, true
}
parsed, err := neturl.Parse(raw)
if err != nil {
return "", false
}
if !strings.EqualFold(parsed.Scheme, targetURL.Scheme) || !strings.EqualFold(parsed.Host, targetURL.Host) {
return "", false
}
return localCaptchaURLForTarget(parsed), true
}
func rewriteProxyHeaderURL(raw string, targetURL *neturl.URL) string {
if raw == "" {
return raw
@ -358,9 +384,10 @@ func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-ch
fmt.Println("\n==============================================")
fmt.Println("ACTION REQUIRED: MANUAL CAPTCHA SOLVING NEEDED")
fmt.Println("Open this URL in your browser: " + captchaURL)
fmt.Println("Open this URL in your browser: " + localCaptchaOrigin())
fmt.Println("==============================================")
fmt.Println()
openBrowser(captchaURL)
key := <-keyCh
@ -441,10 +468,10 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string,
if res.StatusCode >= 300 && res.StatusCode < 400 {
if loc := res.Header.Get("Location"); loc != "" {
if strings.HasPrefix(loc, "/") {
res.Header.Set("Location", loc)
} else if strings.HasPrefix(loc, targetOrigin(targetURL)) {
res.Header.Set("Location", strings.Replace(loc, targetOrigin(targetURL), localCaptchaOrigin(), 1))
if rewritten, ok := rewriteProxyRedirectLocation(loc, targetURL); ok {
res.Header.Set("Location", rewritten)
} else {
res.Header.Del("Location")
}
}
}

65
client/manual_captcha_test.go

@ -0,0 +1,65 @@
package main
import (
"net/url"
"testing"
)
func TestRewriteProxyRedirectLocation(t *testing.T) {
t.Parallel()
targetURL, err := url.Parse("https://id.vk.ru/captcha")
if err != nil {
t.Fatalf("failed to parse target URL: %v", err)
}
testCases := []struct {
name string
location string
want string
ok bool
}{
{
name: "keeps safe relative path",
location: "/captcha?step=2",
want: "/captcha?step=2",
ok: true,
},
{
name: "rewrites same-origin absolute URL",
location: "https://id.vk.ru/captcha?step=2",
want: "http://localhost:8765/captcha?step=2",
ok: true,
},
{
name: "blocks scheme-relative redirect",
location: "//evil.example/captcha",
ok: false,
},
{
name: "blocks slash-backslash redirect",
location: `/\evil.example/captcha`,
ok: false,
},
{
name: "blocks lookalike absolute host",
location: "https://id.vk.ru.evil.example/captcha",
ok: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := rewriteProxyRedirectLocation(tc.location, targetURL)
if ok != tc.ok {
t.Fatalf("rewriteProxyRedirectLocation() ok = %v, want %v", ok, tc.ok)
}
if got != tc.want {
t.Fatalf("rewriteProxyRedirectLocation() = %q, want %q", got, tc.want)
}
})
}
}

918
client/slider_captcha.go

@ -0,0 +1,918 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/color"
_ "image/jpeg"
"io"
"log"
neturl "net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
fhttp "github.com/bogdanfinn/fhttp"
tlsclient "github.com/bogdanfinn/tls-client"
)
const (
captchaDebugInfo = "1d3e9babfd3a74f4588bf90cf5c30d3e8e89a0e2a4544da8de8bbf4d78a32f5c"
sliderCaptchaType = "slider"
defaultSliderAttempts = 4
)
type captchaNotRobotSession struct {
ctx context.Context
sessionToken string
hash string
streamID int
client tlsclient.HttpClient
profile Profile
browserFp string
}
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 string,
hash string,
streamID int,
client tlsclient.HttpClient,
profile Profile,
) *captchaNotRobotSession {
return &captchaNotRobotSession{
ctx: ctx,
sessionToken: sessionToken,
hash: hash,
streamID: streamID,
client: client,
profile: profile,
browserFp: generateBrowserFp(profile),
}
}
func (s *captchaNotRobotSession) baseValues() neturl.Values {
values := neturl.Values{}
values.Set("session_token", s.sessionToken)
values.Set("domain", "vk.com")
values.Set("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
}
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)
values.Set("device", buildCaptchaDeviceJSON(s.profile))
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 status, _ := respObj["status"].(string); status != "" && status != "OK" {
return fmt.Errorf("componentDone status: %s", status)
}
}
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)
}
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(generateSliderCursor(candidateIndex, candidateCount), answer)
}
func (s *captchaNotRobotSession) requestCheck(cursor string, answer string) (*captchaCheckResult, error) {
values := s.baseValues()
values.Set("accelerometer", "[]")
values.Set("gyroscope", "[]")
values.Set("motion", "[]")
values.Set("cursor", cursor)
values.Set("taps", "[]")
values.Set("connectionRtt", "[]")
values.Set("connectionDownlink", "[]")
values.Set("browser_fp", s.browserFp)
values.Set("hash", s.hash)
values.Set("answer", answer)
values.Set("debug_info", captchaDebugInfo)
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,
) (string, error) {
session := newCaptchaNotRobotSession(ctx, sessionToken, hash, streamID, client, profile)
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)
if err := session.requestComponentDone(); 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)
}
sliderContent, err := session.requestSliderContent(sliderSettings)
if err != nil {
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 {
return fmt.Sprintf(
`{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1040,"innerWidth":1920,"innerHeight":969,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"default","userAgent":"%s","platform":"Win32"}`,
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)
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)
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)
result.SuccessToken, _ = respObj["success_token"].(string)
result.ShowCaptchaType, _ = respObj["show_captcha_type"].(string)
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)
if status != "OK" {
return nil, fmt.Errorf("slider getContent status: %s", status)
}
extension, _ := respObj["extension"].(string)
extension = strings.ToLower(extension)
if extension != "jpeg" && extension != "jpg" {
return nil, fmt.Errorf("unsupported slider image format: %s", extension)
}
rawImage, _ := respObj["image"].(string)
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) {
rendered, err := renderSliderCandidate(img, gridSize, mapping)
if err != nil {
return 0, err
}
return scoreRenderedSliderImage(rendered, gridSize), 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 scoreRenderedSliderImage(img image.Image, gridSize int) int64 {
bounds := img.Bounds()
var score int64
for row := 0; row < gridSize; row++ {
for col := 0; col < gridSize-1; col++ {
leftRect := sliderTileRect(bounds, gridSize, row*gridSize+col)
rightRect := sliderTileRect(bounds, gridSize, row*gridSize+col+1)
height := minInt(leftRect.Dy(), rightRect.Dy())
for offset := 0; offset < height; offset++ {
score += pixelDiff(
img.At(leftRect.Max.X-1, leftRect.Min.Y+offset),
img.At(rightRect.Min.X, rightRect.Min.Y+offset),
)
}
}
}
for row := 0; row < gridSize-1; row++ {
for col := 0; col < gridSize; col++ {
topRect := sliderTileRect(bounds, gridSize, row*gridSize+col)
bottomRect := sliderTileRect(bounds, gridSize, (row+1)*gridSize+col)
width := minInt(topRect.Dx(), bottomRect.Dx())
for offset := 0; offset < width; offset++ {
score += pixelDiff(
img.At(topRect.Min.X+offset, topRect.Max.Y-1),
img.At(bottomRect.Min.X+offset, bottomRect.Min.Y),
)
}
}
}
return score
}
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, ",")
}

278
client/slider_captcha_test.go

@ -0,0 +1,278 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"image"
"image/color"
"image/jpeg"
"reflect"
"testing"
)
func TestParseSliderSteps(t *testing.T) {
t.Parallel()
size, swaps, attempts, err := parseSliderSteps([]int{5, 0, 1, 2, 3, 7})
if err != nil {
t.Fatalf("parseSliderSteps returned error: %v", err)
}
if size != 5 {
t.Fatalf("expected size 5, got %d", size)
}
if attempts != 7 {
t.Fatalf("expected attempts 7, got %d", attempts)
}
expected := []int{0, 1, 2, 3}
if !reflect.DeepEqual(swaps, expected) {
t.Fatalf("expected swaps %v, got %v", expected, swaps)
}
}
func TestParseCaptchaSettingsResponseSupportsJSONStringMap(t *testing.T) {
t.Parallel()
resp := map[string]interface{}{
"response": map[string]interface{}{
"show_captcha_type": "checkbox",
"captcha_settings": `{"slider":"slider-token","sound":"sound-token"}`,
},
}
settings, err := parseCaptchaSettingsResponse(resp)
if err != nil {
t.Fatalf("parseCaptchaSettingsResponse returned error: %v", err)
}
if settings.ShowCaptchaType != "checkbox" {
t.Fatalf("expected show_captcha_type checkbox, got %q", settings.ShowCaptchaType)
}
if settings.SettingsByType["slider"] != "slider-token" {
t.Fatalf("expected slider settings token, got %q", settings.SettingsByType["slider"])
}
if settings.SettingsByType["sound"] != "sound-token" {
t.Fatalf("expected sound settings token, got %q", settings.SettingsByType["sound"])
}
}
func TestParseCaptchaBootstrapHTML(t *testing.T) {
t.Parallel()
html := `
<script>
window.init = {"data":{"show_captcha_type":"checkbox","captcha_settings":[{"type":"slider","settings":"slider-token"},{"type":"sound","settings":"sound-token"}]}};
window.lang = {};
</script>
<script>
const powInput = "abc123";
const difficulty = 3;
</script>`
bootstrap, err := parseCaptchaBootstrapHTML(html)
if err != nil {
t.Fatalf("parseCaptchaBootstrapHTML returned error: %v", err)
}
if bootstrap.PowInput != "abc123" {
t.Fatalf("expected pow input abc123, got %q", bootstrap.PowInput)
}
if bootstrap.Difficulty != 3 {
t.Fatalf("expected difficulty 3, got %d", bootstrap.Difficulty)
}
if bootstrap.Settings == nil {
t.Fatal("expected bootstrap settings")
}
if bootstrap.Settings.ShowCaptchaType != "checkbox" {
t.Fatalf("expected show_captcha_type checkbox, got %q", bootstrap.Settings.ShowCaptchaType)
}
if bootstrap.Settings.SettingsByType["slider"] != "slider-token" {
t.Fatalf("expected slider token, got %q", bootstrap.Settings.SettingsByType["slider"])
}
}
func TestRenderSliderCandidateMatchesSwapLayout(t *testing.T) {
t.Parallel()
src := image.NewRGBA(image.Rect(0, 0, 20, 20))
fillRect(src, image.Rect(0, 0, 10, 10), color.RGBA{R: 255, A: 255})
fillRect(src, image.Rect(10, 0, 20, 10), color.RGBA{G: 255, A: 255})
fillRect(src, image.Rect(0, 10, 10, 20), color.RGBA{B: 255, A: 255})
fillRect(src, image.Rect(10, 10, 20, 20), color.RGBA{R: 255, G: 255, A: 255})
mapping, err := buildSliderTileMapping(2, []int{0, 1})
if err != nil {
t.Fatalf("buildSliderTileMapping returned error: %v", err)
}
rendered, err := renderSliderCandidate(src, 2, mapping)
if err != nil {
t.Fatalf("renderSliderCandidate returned error: %v", err)
}
assertPixelEquals(t, rendered.At(2, 2), color.RGBA{G: 255, A: 255})
assertPixelEquals(t, rendered.At(12, 2), color.RGBA{R: 255, A: 255})
assertPixelEquals(t, rendered.At(2, 12), color.RGBA{B: 255, A: 255})
assertPixelEquals(t, rendered.At(12, 12), color.RGBA{R: 255, G: 255, A: 255})
}
func TestRankSliderCandidatesPrefersMostCoherentImage(t *testing.T) {
t.Parallel()
src := image.NewRGBA(image.Rect(0, 0, 30, 30))
for y := 0; y < 30; y++ {
for x := 0; x < 30; x++ {
src.Set(x, y, color.RGBA{
R: uint8(x * 5),
G: uint8(y * 5),
B: uint8((x + y) * 3),
A: 255,
})
}
}
candidates, err := rankSliderCandidates(src, 3, []int{0, 1, 0, 1})
if err != nil {
t.Fatalf("rankSliderCandidates returned error: %v", err)
}
if len(candidates) != 2 {
t.Fatalf("expected 2 candidates, got %d", len(candidates))
}
if candidates[0].Index != 2 {
t.Fatalf("expected solved candidate to rank first, got candidate %d", candidates[0].Index)
}
}
func TestEncodeSliderAnswer(t *testing.T) {
t.Parallel()
encoded, err := encodeSliderAnswer([]int{9, 10, 2})
if err != nil {
t.Fatalf("encodeSliderAnswer returned error: %v", err)
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
t.Fatalf("failed to decode answer: %v", err)
}
var payload struct {
Value []int `json:"value"`
}
if err := json.Unmarshal(decoded, &payload); err != nil {
t.Fatalf("failed to unmarshal answer payload: %v", err)
}
expected := []int{9, 10, 2}
if !reflect.DeepEqual(payload.Value, expected) {
t.Fatalf("expected payload %v, got %v", expected, payload.Value)
}
}
func TestTrySliderCaptchaCandidates(t *testing.T) {
t.Parallel()
candidates := []sliderCandidate{
{Index: 1, ActiveSteps: []int{0, 1}, Score: 10},
{Index: 2, ActiveSteps: []int{0, 1, 0, 1}, Score: 20},
}
t.Run("success on first candidate", func(t *testing.T) {
token, err := trySliderCaptchaCandidates(candidates, 2, func(candidate sliderCandidate) (*captchaCheckResult, error) {
if candidate.Index != 1 {
t.Fatalf("unexpected candidate index %d", candidate.Index)
}
return &captchaCheckResult{Status: "OK", SuccessToken: "token-1"}, nil
})
if err != nil {
t.Fatalf("trySliderCaptchaCandidates returned error: %v", err)
}
if token != "token-1" {
t.Fatalf("expected token-1, got %s", token)
}
})
t.Run("success on later candidate", func(t *testing.T) {
calls := 0
token, err := trySliderCaptchaCandidates(candidates, 2, func(candidate sliderCandidate) (*captchaCheckResult, error) {
calls++
if candidate.Index == 1 {
return &captchaCheckResult{Status: "BOT"}, nil
}
return &captchaCheckResult{Status: "OK", SuccessToken: "token-2"}, nil
})
if err != nil {
t.Fatalf("trySliderCaptchaCandidates returned error: %v", err)
}
if calls != 2 {
t.Fatalf("expected 2 calls, got %d", calls)
}
if token != "token-2" {
t.Fatalf("expected token-2, got %s", token)
}
})
t.Run("exhausted candidates", func(t *testing.T) {
_, err := trySliderCaptchaCandidates(candidates, 1, func(candidate sliderCandidate) (*captchaCheckResult, error) {
return &captchaCheckResult{Status: "BOT"}, nil
})
if err == nil {
t.Fatal("expected error after exhausting ranked candidates")
}
})
}
func TestParseSliderCaptchaContentResponse(t *testing.T) {
t.Parallel()
src := image.NewRGBA(image.Rect(0, 0, 20, 20))
fillRect(src, src.Bounds(), color.RGBA{R: 12, G: 34, B: 56, A: 255})
var buf bytes.Buffer
if err := jpeg.Encode(&buf, src, nil); err != nil {
t.Fatalf("failed to encode jpeg fixture: %v", err)
}
resp := map[string]interface{}{
"response": map[string]interface{}{
"status": "OK",
"extension": "jpeg",
"image": base64.StdEncoding.EncodeToString(buf.Bytes()),
"steps": []interface{}{float64(5), float64(0), float64(1), float64(2), float64(3), float64(6)},
},
}
content, err := parseSliderCaptchaContentResponse(resp)
if err != nil {
t.Fatalf("parseSliderCaptchaContentResponse returned error: %v", err)
}
if content.Size != 5 {
t.Fatalf("expected size 5, got %d", content.Size)
}
if content.Attempts != 6 {
t.Fatalf("expected attempts 6, got %d", content.Attempts)
}
if len(content.Steps) != 4 {
t.Fatalf("expected 4 swap entries, got %d", len(content.Steps))
}
}
func fillRect(img *image.RGBA, rect image.Rectangle, c color.Color) {
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
img.Set(x, y, c)
}
}
}
func assertPixelEquals(t *testing.T, actual color.Color, expected color.RGBA) {
t.Helper()
ar, ag, ab, aa := actual.RGBA()
if ar != uint32(expected.R)*0x101 || ag != uint32(expected.G)*0x101 || ab != uint32(expected.B)*0x101 || aa != uint32(expected.A)*0x101 {
t.Fatalf("expected pixel %+v, got rgba(%d,%d,%d,%d)", expected, ar, ag, ab, aa)
}
}

27
go.mod

@ -9,16 +9,16 @@ require (
github.com/cbeuw/connutil v1.0.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/pion/dtls/v3 v3.0.11
github.com/pion/dtls/v3 v3.1.2
github.com/pion/logging v0.2.4
github.com/pion/transport/v4 v4.0.1
github.com/pion/turn/v5 v5.0.2
github.com/pion/turn/v5 v5.0.3
github.com/xtaci/kcp-go/v5 v5.6.18
github.com/xtaci/smux v1.5.34
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bdandy/go-errors v1.2.2 // indirect
github.com/bdandy/go-socks4 v1.2.3 // indirect
github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect
@ -26,12 +26,12 @@ require (
github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/reedsolomon v1.12.4 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.1.1 // indirect
github.com/pion/stun/v3 v3.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
@ -39,12 +39,11 @@ require (
github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
)

68
go.sum

@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
@ -51,26 +51,26 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons=
github.com/pion/dtls/v3 v3.0.11/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/turn/v5 v5.0.2 h1:GHlDk+fiegz+yibb3ch+tK+iPFokoVWiM+aVJakySqA=
github.com/pion/turn/v5 v5.0.2/go.mod h1:cumcsSEF2ytAtDhDwkYgYhv1uJ3AOP7a4pFt0NL/snY=
github.com/pion/turn/v5 v5.0.3 h1:I+Nw0fQgdPWF1SXDj0egWDhCkcff7gWiigdQpOK52Ak=
github.com/pion/turn/v5 v5.0.3/go.mod h1:fs4SogUh/aRGQzonc4Lx3Jp4EU3j3t0PfNDEd9KcD/w=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -103,14 +103,14 @@ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -118,58 +118,58 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

22
server/main.go

@ -2,7 +2,6 @@ package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"io"
@ -51,16 +50,19 @@ func main() {
panic(genErr)
}
// Prepare the configuration of the DTLS connection
config := &dtls.Config{
Certificates: []tls.Certificate{certificate},
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
ConnectionIDGenerator: dtls.RandomCIDGenerator(8),
}
//
// Everything below is the pion-DTLS API! Thanks for using it ❤️.
//
// Listen for DTLS connections
listener, err := dtls.Listen("udp", addr, config)
// Connect to a DTLS server
listener, err := dtls.ListenWithOptions(
"udp",
addr,
dtls.WithCertificates(certificate),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
dtls.WithConnectionIDGenerator(dtls.RandomCIDGenerator(8)),
)
if err != nil {
panic(err)
}

Loading…
Cancel
Save