Browse Source

feat(client): add captcha host flag

Expose manual captcha on a configured host:port for router and OpenWrt setups.

Refs #157
pull/162/head
Moroka8 3 weeks ago
parent
commit
aae42b1283
  1. 1
      README.md
  2. 1
      pkg/clientcore/cli.go
  3. 4
      pkg/clientcore/main.go
  4. 67
      pkg/clientcore/manual_captcha.go
  5. 52
      pkg/clientcore/manual_captcha_test.go

1
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` | подробные логи |

1
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 {

4
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

67
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
}

52
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)
}
})
}
}

Loading…
Cancel
Save