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.
363 lines
11 KiB
363 lines
11 KiB
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bschaatsbergen/dnsdialer"
|
|
"github.com/cacggghp/vk-turn-proxy/internal/cliutil"
|
|
)
|
|
|
|
func TestParseClientOptionsShowsUsageWithoutArgs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
_, exitCode := parseClientOptions(nil, "client", &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want 0", exitCode)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected no stderr output, got %q", stderr.String())
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, "Usage:\n client -peer <host:port> -vk-link <link> [flags]") {
|
|
t.Fatalf("usage output missing client help text: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsShowsHelpFlagUsage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
_, exitCode := parseClientOptions([]string{"-help"}, "client", &stdout, &stderr)
|
|
if exitCode != 0 {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want 0", exitCode)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected no stderr output, got %q", stderr.String())
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, "Examples:") {
|
|
t.Fatalf("expected help examples in output, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsRequiresPeer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
_, exitCode := parseClientOptions([]string{"-vk-link", "https://vk.com/call/join/test"}, "client", &stdout, &stderr)
|
|
if exitCode != 2 {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want 2", exitCode)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("expected no stdout output, got %q", stdout.String())
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "error: -peer is required") {
|
|
t.Fatalf("expected missing peer error, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsParsesValidVKArgs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
opts, exitCode := parseClientOptions([]string{"-peer", "127.0.0.1:56000", "-vk-link", "https://vk.com/call/join/test", "-listen", "127.0.0.1:9001"}, "client", &stdout, &stderr)
|
|
if exitCode != cliutil.ContinueExecution {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want %d", exitCode, cliutil.ContinueExecution)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected no stderr output, got %q", stderr.String())
|
|
}
|
|
if opts.peerAddr != "127.0.0.1:56000" {
|
|
t.Fatalf("peerAddr = %q, want 127.0.0.1:56000", opts.peerAddr)
|
|
}
|
|
if opts.vklink != "https://vk.com/call/join/test" {
|
|
t.Fatalf("vklink = %q, want VK link", opts.vklink)
|
|
}
|
|
if opts.listen != "127.0.0.1:9001" {
|
|
t.Fatalf("listen = %q, want 127.0.0.1:9001", opts.listen)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsParsesTelemostDataChannelArgs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
opts, exitCode := parseClientOptions([]string{
|
|
"-yandex-link", "https://telemost.yandex.ru/j/test",
|
|
"-listen", "127.0.0.1:9002",
|
|
"-dc",
|
|
}, "client", &stdout, &stderr)
|
|
if exitCode != cliutil.ContinueExecution {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want %d", exitCode, cliutil.ContinueExecution)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected no stderr output, got %q", stderr.String())
|
|
}
|
|
if !opts.dc {
|
|
t.Fatal("dc = false, want true")
|
|
}
|
|
if opts.peerAddr != "" {
|
|
t.Fatalf("peerAddr = %q, want empty for dc mode", opts.peerAddr)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsRejectsDataChannelWithoutDataChannelRoom(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
_, exitCode := parseClientOptions([]string{
|
|
"-vk-link", "https://vk.com/call/join/test",
|
|
"-dc",
|
|
}, "client", &stdout, &stderr)
|
|
if exitCode != 2 {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want 2", exitCode)
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "-dc requires -yandex-link or -jazz-room") {
|
|
t.Fatalf("expected dc validation error, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsAllowsTelemostDataChannelWithVLESS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
opts, exitCode := parseClientOptions([]string{
|
|
"-yandex-link", "https://telemost.yandex.ru/j/test",
|
|
"-dc",
|
|
"-vless",
|
|
}, "client", &stdout, &stderr)
|
|
if exitCode != cliutil.ContinueExecution {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want %d", exitCode, cliutil.ContinueExecution)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected no stderr output, got %q", stderr.String())
|
|
}
|
|
if !opts.dc || !opts.vlessMode {
|
|
t.Fatalf("expected dc and vlessMode to be true, got dc=%v vlessMode=%v", opts.dc, opts.vlessMode)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsParsesJazzDataChannelArgs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
opts, exitCode := parseClientOptions([]string{
|
|
"-jazz-room", "room:password",
|
|
"-listen", "127.0.0.1:9002",
|
|
"-dc",
|
|
}, "client", &stdout, &stderr)
|
|
if exitCode != cliutil.ContinueExecution {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want %d", exitCode, cliutil.ContinueExecution)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("expected no stderr output, got %q", stderr.String())
|
|
}
|
|
if !opts.dc {
|
|
t.Fatal("dc = false, want true")
|
|
}
|
|
if opts.jazzRoom != "room:password" {
|
|
t.Fatalf("jazzRoom = %q, want room:password", opts.jazzRoom)
|
|
}
|
|
if opts.peerAddr != "" {
|
|
t.Fatalf("peerAddr = %q, want empty for dc mode", opts.peerAddr)
|
|
}
|
|
}
|
|
|
|
func TestParseClientOptionsRejectsJazzRoomWithoutDataChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
_, exitCode := parseClientOptions([]string{
|
|
"-peer", "127.0.0.1:56000",
|
|
"-jazz-room", "room:password",
|
|
}, "client", &stdout, &stderr)
|
|
if exitCode != 2 {
|
|
t.Fatalf("parseClientOptions() exitCode = %d, want 2", exitCode)
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "-jazz-room requires -dc") {
|
|
t.Fatalf("expected jazz room validation error, got %q", got)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCaptchaLogAttempt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := captchaLogAttempt(context.Background(), captchaSolveModeManual, 0); got != 1 {
|
|
t.Fatalf("captchaLogAttempt() = %d, want 1 for first manual attempt", got)
|
|
}
|
|
|
|
ctx := withCaptchaFailureCount(context.Background(), 1)
|
|
if got := captchaLogAttempt(ctx, captchaSolveModeManual, 0); got != 2 {
|
|
t.Fatalf("captchaLogAttempt() = %d, want 2 for second bucket manual attempt", got)
|
|
}
|
|
|
|
if got := captchaLogAttempt(ctx, captchaSolveModeManual, 2); got != 3 {
|
|
t.Fatalf("captchaLogAttempt() = %d, want 3 when in-request attempt count is already higher", got)
|
|
}
|
|
|
|
if got := captchaLogAttempt(ctx, captchaSolveModeAuto, 0); got != 1 {
|
|
t.Fatalf("captchaLogAttempt() = %d, want 1 for auto attempt count", got)
|
|
}
|
|
}
|
|
|
|
func TestGetVkCredsCachedDisablesBucketAfterTwoCaptchaFailures(t *testing.T) {
|
|
credentialsStore.mu.Lock()
|
|
previousCaches := credentialsStore.caches
|
|
credentialsStore.caches = make(map[int]*StreamCredentialsCache)
|
|
credentialsStore.mu.Unlock()
|
|
defer func() {
|
|
credentialsStore.mu.Lock()
|
|
credentialsStore.caches = previousCaches
|
|
credentialsStore.mu.Unlock()
|
|
}()
|
|
|
|
previousFetch := fetchVkCredsSerializedFunc
|
|
defer func() {
|
|
fetchVkCredsSerializedFunc = previousFetch
|
|
}()
|
|
previousConfiguredStreams := configuredStreams.Load()
|
|
configuredStreams.Store(20)
|
|
defer configuredStreams.Store(previousConfiguredStreams)
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
callCount int
|
|
)
|
|
fetchVkCredsSerializedFunc = func(ctx context.Context, link string, streamID int, dialer *dnsdialer.Dialer) (string, string, string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
callCount++
|
|
switch callCount {
|
|
case 1:
|
|
return "", "", "", fmt.Errorf("CAPTCHA_WAIT_REQUIRED")
|
|
case 2:
|
|
return "", "", "", fmt.Errorf("CAPTCHA_WAIT_REQUIRED")
|
|
default:
|
|
return "", "", "", fmt.Errorf("unexpected extra fetch for stream %d", streamID)
|
|
}
|
|
}
|
|
|
|
_, _, _, err := getVkCredsCached(context.Background(), "link", 10, nil)
|
|
if err == nil || err.Error() != "CAPTCHA_WAIT_REQUIRED" {
|
|
t.Fatalf("first getVkCredsCached() error = %v, want CAPTCHA_WAIT_REQUIRED", err)
|
|
}
|
|
|
|
_, _, _, err = getVkCredsCached(context.Background(), "link", 17, nil)
|
|
if err == nil || err.Error() != "CAPTCHA_WAIT_REQUIRED" {
|
|
t.Fatalf("second getVkCredsCached() error = %v, want shared CAPTCHA_WAIT_REQUIRED", err)
|
|
}
|
|
|
|
mu.Lock()
|
|
if callCount != 1 {
|
|
mu.Unlock()
|
|
t.Fatalf("expected one fetch attempt during shared captcha cooldown, got %d", callCount)
|
|
}
|
|
mu.Unlock()
|
|
|
|
cache := getStreamCache(10)
|
|
cache.mutex.Lock()
|
|
cache.retryAfter = time.Now().Add(-time.Second)
|
|
cache.mutex.Unlock()
|
|
|
|
_, _, _, err = getVkCredsCached(context.Background(), "link", 19, nil)
|
|
if err == nil || !strings.Contains(err.Error(), errCaptchaBucketDisabled.Error()) {
|
|
t.Fatalf("third getVkCredsCached() error = %v, want bucket disabled", err)
|
|
}
|
|
|
|
_, _, _, err = getVkCredsCached(context.Background(), "link", 11, nil)
|
|
if err == nil || !strings.Contains(err.Error(), errCaptchaBucketDisabled.Error()) {
|
|
t.Fatalf("fourth getVkCredsCached() error = %v, want bucket disabled without new fetch", err)
|
|
}
|
|
|
|
mu.Lock()
|
|
if callCount != 2 {
|
|
mu.Unlock()
|
|
t.Fatalf("expected exactly two fetch attempts before bucket disable, got %d", callCount)
|
|
}
|
|
mu.Unlock()
|
|
|
|
if got := activeConfiguredStreamCount(); got != 10 {
|
|
t.Fatalf("activeConfiguredStreamCount() = %d, want 10 after disabling second bucket", got)
|
|
}
|
|
}
|
|
|