package clientcore import ( "bytes" "compress/gzip" "context" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "net/http/httputil" neturl "net/url" "os/exec" "regexp" "runtime" "strings" "time" "github.com/bschaatsbergen/dnsdialer" ) 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 (&neturl.URL{Scheme: "http", Host: localCaptchaHost()}).String() } func localCaptchaListenAddrs() []string { addrs := []string{ "127.0.0.1:" + captchaListenPort, "[::1]:" + captchaListenPort, } if customCaptchaHost != "" { addrs = appendUniqueFold(addrs, customCaptchaHost) } return addrs } func localCaptchaHosts() []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 { 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: localCaptchaHost(), 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 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 } 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") req.Header.Del("TE") // Disable transfer encoding compression 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()) } } // htmlURLAttrDoubleRe matches src/href/action attributes with double-quoted absolute or protocol-relative URLs. var htmlURLAttrDoubleRe = regexp.MustCompile(`(?i)((?:src|href|action)\s*=\s*)"((?:https?:)?//[^"]+)"`) // htmlURLAttrSingleRe matches src/href/action attributes with single-quoted absolute or protocol-relative URLs. var htmlURLAttrSingleRe = regexp.MustCompile(`(?i)((?:src|href|action)\s*=\s*)'((?:https?:)?//[^']+)'`) // htmlScriptContentRe matches )`) // htmlStyleContentRe matches )`) // rewriteHTMLAttrsServerSide rewrites absolute and protocol-relative URLs in src/href/action // attributes of raw HTML. URLs matching the upstream origin are redirected to localhost; // all other absolute URLs are routed through /generic_proxy so that cross-domain resources // (st.vk.com, userapi.com, etc.) load correctly through the proxy. func rewriteHTMLAttrsServerSide(html string, targetURL *neturl.URL) string { localOrigin := localCaptchaOrigin() upstreamOrigin := targetOrigin(targetURL) rewriteURL := func(rawURL string) string { // Normalise protocol-relative URL to absolute using the upstream scheme absURL := rawURL if strings.HasPrefix(rawURL, "//") { absURL = targetURL.Scheme + ":" + rawURL } if strings.HasPrefix(absURL, upstreamOrigin) { return localOrigin + absURL[len(upstreamOrigin):] } // Already points to local proxy — leave as-is if strings.HasPrefix(absURL, localOrigin) { return rawURL } // Any other absolute URL → route through generic_proxy return "/generic_proxy?proxy_url=" + neturl.QueryEscape(absURL) } var placeholders = make(map[string]string) html = htmlScriptContentRe.ReplaceAllStringFunc(html, func(match string) string { groups := htmlScriptContentRe.FindStringSubmatch(match) if len(groups) < 4 { return match } id := fmt.Sprintf("@@CONTENT_%d@@", len(placeholders)) placeholders[id] = groups[2] return groups[1] + id + groups[3] }) html = htmlStyleContentRe.ReplaceAllStringFunc(html, func(match string) string { groups := htmlStyleContentRe.FindStringSubmatch(match) if len(groups) < 4 { return match } id := fmt.Sprintf("@@CONTENT_%d@@", len(placeholders)) placeholders[id] = groups[2] return groups[1] + id + groups[3] }) html = htmlURLAttrDoubleRe.ReplaceAllStringFunc(html, func(match string) string { groups := htmlURLAttrDoubleRe.FindStringSubmatch(match) if len(groups) < 3 { return match } return groups[1] + `"` + rewriteURL(groups[2]) + `"` }) html = htmlURLAttrSingleRe.ReplaceAllStringFunc(html, func(match string) string { groups := htmlURLAttrSingleRe.FindStringSubmatch(match) if len(groups) < 3 { return match } return groups[1] + `'` + rewriteURL(groups[2]) + `'` }) for id, content := range placeholders { html = strings.Replace(html, id, content, 1) } return html } func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string { localOrigin := localCaptchaOrigin() upstreamOrigin := targetOrigin(targetURL) // Step 1: plain text replacement for the primary upstream origin html = strings.ReplaceAll(html, upstreamOrigin, localOrigin) // Step 2: rewrite all other absolute URLs in HTML attributes server-side. // This is critical: the browser begins downloading `, localOrigin, upstreamOrigin) // Step 3: inject the client-side script as early as possible — at the opening tag // so that XHR/fetch overrides are active before any inline