package main import ( "bytes" "compress/gzip" "context" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "net/http/httputil" neturl "net/url" "os/exec" "runtime" "strings" "time" "github.com/bschaatsbergen/dnsdialer" ) const captchaListenPort = "8765" type browserCommand struct { name string args []string } func localCaptchaOrigin() string { return "http://localhost:" + captchaListenPort } func localCaptchaListenAddrs() []string { return []string{ "127.0.0.1:" + captchaListenPort, "[::1]:" + captchaListenPort, } } func localCaptchaHosts() []string { return []string{ "localhost:" + captchaListenPort, "127.0.0.1:" + captchaListenPort, "[::1]:" + captchaListenPort, } } func isLocalCaptchaHost(host string) bool { for _, localHost := range localCaptchaHosts() { if strings.EqualFold(host, localHost) { return true } } return false } func localCaptchaURLForTarget(targetURL *neturl.URL) string { localURL := &neturl.URL{ Scheme: "http", Host: "localhost:" + captchaListenPort, Path: targetURL.Path, RawPath: targetURL.RawPath, RawQuery: targetURL.RawQuery, } if localURL.Path == "" { localURL.Path = "/" } return localURL.String() } func targetOrigin(targetURL *neturl.URL) string { return targetURL.Scheme + "://" + targetURL.Host } func rewriteProxyHeaderURL(raw string, targetURL *neturl.URL) string { if raw == "" { return raw } parsed, err := neturl.Parse(raw) if err != nil { return raw } if parsed.Scheme != "http" || !isLocalCaptchaHost(parsed.Host) { return raw } parsed.Scheme = targetURL.Scheme parsed.Host = targetURL.Host return parsed.String() } func rewriteProxyRequest(req *http.Request, targetURL *neturl.URL) { req.URL.Scheme = targetURL.Scheme req.URL.Host = targetURL.Host if req.URL.Path == "" { req.URL.Path = targetURL.Path } req.Host = targetURL.Host req.Header.Del("Accept-Encoding") for _, headerName := range []string{"Origin", "Referer"} { if rewritten := rewriteProxyHeaderURL(req.Header.Get(headerName), targetURL); rewritten != "" { req.Header.Set(headerName, rewritten) } else { req.Header.Del(headerName) } } } func extractSuccessToken(body []byte) string { var payload struct { Response struct { SuccessToken string `json:"success_token"` } `json:"response"` } if err := json.Unmarshal(body, &payload); err != nil { return "" } return payload.Response.SuccessToken } func rewriteProxyCookies(header http.Header) { cookies := (&http.Response{Header: header}).Cookies() if len(cookies) == 0 { return } header.Del("Set-Cookie") for _, cookie := range cookies { cookie.Domain = "" cookie.Secure = false cookie.Partitioned = false if cookie.SameSite == http.SameSiteNoneMode || cookie.SameSite == http.SameSiteStrictMode { cookie.SameSite = http.SameSiteLaxMode } header.Add("Set-Cookie", cookie.String()) } } func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string { localOrigin := localCaptchaOrigin() upstreamOrigin := targetOrigin(targetURL) html = strings.ReplaceAll(html, upstreamOrigin, localOrigin) script := fmt.Sprintf(` `, localOrigin, upstreamOrigin) switch { case strings.Contains(html, ""): return strings.Replace(html, "", script+"", 1) case strings.Contains(html, "
"): return strings.Replace(html, "", script+"", 1) default: return html + script } } func newCaptchaProxyTransport(dialer *dnsdialer.Dialer) *http.Transport { transport := &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, ForceAttemptHTTP2: true, } if dialer != nil { transport.DialContext = dialer.DialContext } return transport } func startCaptchaServer(srv *http.Server, logPrefix string) error { var listenErrs []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)) continue } listening = true go func(listener net.Listener) { if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Printf("%s: %s", logPrefix, err) } }(listener) } if listening { return nil } return fmt.Errorf("captcha listeners failed: %s", strings.Join(listenErrs, "; ")) } // runCaptchaServerAndWait triggers the browser, and waiting gracefully for the solution token. func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-chan string, logPrefix string) (string, error) { srv := &http.Server{Handler: handler} if err := startCaptchaServer(srv, logPrefix); err != nil { return "", err } fmt.Println("\n==============================================") fmt.Println("ACTION REQUIRED: MANUAL CAPTCHA SOLVING NEEDED") fmt.Println("Open this URL in your browser: " + captchaURL) fmt.Println("==============================================") fmt.Println() openBrowser(captchaURL) key := <-keyCh ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { return "", err } return key, nil } // notifyKey pushes the key string to the given channel without blocking func notifyKey(keyCh chan<- string, key string) { if key != "" { select { case keyCh <- key: default: } } } func solveCaptchaViaHTTP(captchaImg string) (string, error) { keyCh := make(chan string, 1) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = fmt.Fprintf(w, `