diff --git a/README.md b/README.md index 853aea6..6251f79 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,7 @@ docker build -t vk-turn-proxy . | `-wrap-key` | пусто | 32-байтный ключ в hex, 64 символа | | `-gen-wrap-key` | `false` | напечатать новый WRAP-ключ и выйти | | `-manual-captcha` | `false` | сразу использовать ручное прохождение captcha | +| `-captcha-host` | пусто | host:port для manual captcha, например `192.168.99.1:8765` | | `-captcha-solver` | `v2` | авто-решатель captcha: `v1` или `v2` | | `-streams-per-cred` | `10` | сколько потоков используют один кеш TURN-учетных данных | | `-debug` | `false` | подробные логи | diff --git a/pkg/clientcore/cli.go b/pkg/clientcore/cli.go index 57a843e..fd71c9e 100644 --- a/pkg/clientcore/cli.go +++ b/pkg/clientcore/cli.go @@ -49,6 +49,7 @@ func RunCLI() { flag.BoolVar(&cfg.Debug, "debug", false, "enable debug logging") flag.BoolVar(&cfg.ManualCaptcha, "manual-captcha", false, "skip auto captcha solving, use manual mode immediately") flag.StringVar(&cfg.CaptchaSolver, "captcha-solver", "v2", "auto captcha solver implementation: v1|v2") + flag.StringVar(&cfg.CaptchaHost, "captcha-host", "", "manual captcha host:port to expose in addition to localhost:8765") flag.Parse() if *genWrapKey { diff --git a/pkg/clientcore/main.go b/pkg/clientcore/main.go index b63fdab..665a93f 100644 --- a/pkg/clientcore/main.go +++ b/pkg/clientcore/main.go @@ -2148,6 +2148,7 @@ type Config struct { Debug bool `json:"debug,omitempty"` ManualCaptcha bool `json:"manual_captcha,omitempty"` CaptchaSolver string `json:"captcha_solver,omitempty"` + CaptchaHost string `json:"captcha_host,omitempty"` } func (cfg *Config) setDefaults() { @@ -2200,6 +2201,9 @@ func Run(ctx context.Context, cfg Config) error { if captchaSolverVersion != "v1" && captchaSolverVersion != "v2" { captchaSolverVersion = "v2" } + if err := setLocalCaptchaHost(cfg.CaptchaHost); err != nil { + return err + } autoCaptchaSliderPOC = !manualCaptcha var link string diff --git a/pkg/clientcore/manual_captcha.go b/pkg/clientcore/manual_captcha.go index 21393fc..588e2a7 100644 --- a/pkg/clientcore/manual_captcha.go +++ b/pkg/clientcore/manual_captcha.go @@ -24,28 +24,79 @@ import ( const captchaListenPort = "8765" +var customCaptchaHost string + type browserCommand struct { name string args []string } +func setLocalCaptchaHost(host string) error { + host = strings.TrimSpace(host) + if host == "" { + customCaptchaHost = "" + return nil + } + if strings.Contains(host, "://") { + return fmt.Errorf("-captcha-host must be host:port without scheme") + } + hostname, port, err := net.SplitHostPort(host) + if err != nil { + return fmt.Errorf("-captcha-host must be host:port: %w", err) + } + if hostname == "" || port == "" { + return fmt.Errorf("-captcha-host must include both host and port") + } + + u := &neturl.URL{Scheme: "http", Host: host} + if u.String() == "" { + return fmt.Errorf("-captcha-host is invalid") + } + customCaptchaHost = host + return nil +} + +func localCaptchaHost() string { + if customCaptchaHost != "" { + return customCaptchaHost + } + return "localhost:" + captchaListenPort +} + func localCaptchaOrigin() string { - return "http://localhost:" + captchaListenPort + return (&neturl.URL{Scheme: "http", Host: localCaptchaHost()}).String() } func localCaptchaListenAddrs() []string { - return []string{ + addrs := []string{ "127.0.0.1:" + captchaListenPort, "[::1]:" + captchaListenPort, } + if customCaptchaHost != "" { + addrs = appendUniqueFold(addrs, customCaptchaHost) + } + return addrs } func localCaptchaHosts() []string { - return []string{ + hosts := []string{ "localhost:" + captchaListenPort, "127.0.0.1:" + captchaListenPort, "[::1]:" + captchaListenPort, } + if customCaptchaHost != "" { + hosts = appendUniqueFold(hosts, customCaptchaHost) + } + return hosts +} + +func appendUniqueFold(values []string, value string) []string { + for _, existing := range values { + if strings.EqualFold(existing, value) { + return values + } + } + return append(values, value) } func isLocalCaptchaHost(host string) bool { @@ -60,7 +111,7 @@ func isLocalCaptchaHost(host string) bool { func localCaptchaURLForTarget(targetURL *neturl.URL) string { localURL := &neturl.URL{ Scheme: "http", - Host: "localhost:" + captchaListenPort, + Host: localCaptchaHost(), Path: targetURL.Path, RawPath: targetURL.RawPath, RawQuery: targetURL.RawQuery, @@ -484,12 +535,16 @@ func newCaptchaProxyTransport(dialer *dnsdialer.Dialer) *http.Transport { func startCaptchaServer(srv *http.Server, logPrefix string) error { var listenErrs []string + var customListenErr string var listening bool for _, addr := range localCaptchaListenAddrs() { listener, err := net.Listen("tcp", addr) if err != nil { listenErrs = append(listenErrs, fmt.Sprintf("%s (%v)", addr, err)) + if customCaptchaHost != "" && strings.EqualFold(addr, customCaptchaHost) { + customListenErr = fmt.Sprintf("%s (%v)", addr, err) + } continue } listening = true @@ -505,6 +560,10 @@ func startCaptchaServer(srv *http.Server, logPrefix string) error { }(wrappedListener) } + if customListenErr != "" { + return fmt.Errorf("captcha listener failed: %s", customListenErr) + } + if listening { return nil } diff --git a/pkg/clientcore/manual_captcha_test.go b/pkg/clientcore/manual_captcha_test.go index abb1105..c178f2a 100644 --- a/pkg/clientcore/manual_captcha_test.go +++ b/pkg/clientcore/manual_captcha_test.go @@ -6,8 +6,6 @@ import ( ) 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) @@ -51,8 +49,6 @@ func TestRewriteProxyRedirectLocation(t *testing.T) { 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) @@ -63,3 +59,51 @@ func TestRewriteProxyRedirectLocation(t *testing.T) { }) } } + +func TestCustomCaptchaHost(t *testing.T) { + if err := setLocalCaptchaHost("192.168.99.1:8765"); err != nil { + t.Fatalf("setLocalCaptchaHost() failed: %v", err) + } + defer func() { + if err := setLocalCaptchaHost(""); err != nil { + t.Fatalf("reset local captcha host: %v", err) + } + }() + + targetURL, err := url.Parse("https://id.vk.ru/captcha?step=2") + if err != nil { + t.Fatalf("failed to parse target URL: %v", err) + } + + if got, want := localCaptchaOrigin(), "http://192.168.99.1:8765"; got != want { + t.Fatalf("localCaptchaOrigin() = %q, want %q", got, want) + } + if got, want := localCaptchaURLForTarget(targetURL), "http://192.168.99.1:8765/captcha?step=2"; got != want { + t.Fatalf("localCaptchaURLForTarget() = %q, want %q", got, want) + } + if !isLocalCaptchaHost("192.168.99.1:8765") { + t.Fatal("custom captcha host should be accepted as local") + } + + addrs := localCaptchaListenAddrs() + if len(addrs) != 3 || addrs[2] != "192.168.99.1:8765" { + t.Fatalf("localCaptchaListenAddrs() = %v, want custom host appended", addrs) + } +} + +func TestSetLocalCaptchaHostRejectsInvalidValues(t *testing.T) { + testCases := []string{ + "http://192.168.99.1:8765", + "192.168.99.1", + ":8765", + } + + for _, tc := range testCases { + tc := tc + t.Run(tc, func(t *testing.T) { + if err := setLocalCaptchaHost(tc); err == nil { + t.Fatalf("setLocalCaptchaHost(%q) succeeded, want error", tc) + } + }) + } +}