package main import ( "bytes" "compress/gzip" "context" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "net/http/httputil" neturl "net/url" "os/exec" "regexp" "runtime" "sort" "strings" "time" ) const captchaListenPort = "8765" // redactSensitiveQueryRe matches sensitive token/hash params in form bodies and // query strings. Replaced with "" so logs reveal presence and length // without exposing the JWT itself. var redactSensitiveQueryRe = regexp.MustCompile(`(?i)\b(session_token|access_token|success_token|hash|debug_info|browser_fp)=([^&\s]*)`) var redactCookieValueRe = regexp.MustCompile(`(remix[a-z]+|prcl|domain_sid)=([^;\s]+)`) func redactBodyForLog(s string) string { return redactSensitiveQueryRe.ReplaceAllStringFunc(s, func(m string) string { groups := redactSensitiveQueryRe.FindStringSubmatch(m) if len(groups) < 3 { return m } return groups[1] + "=" }) } func redactHeaderForLog(name, value string) string { switch strings.ToLower(name) { case "cookie", "set-cookie": return redactCookieValueRe.ReplaceAllString(value, "$1=") case "referer", "origin", "location": return redactBodyForLog(value) case "authorization", "proxy-authorization": return "" } return value } 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 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") // Strip WebView identity / fingerprint leak headers. Android WebView // auto-injects X-Requested-With with the host package name, which would // reveal the proxy app to VK. for _, h := range []string{ "X-Requested-With", "X-Android-Package", "X-Android-Cert", "X-Client-Data", "X-Discord-Locale", "X-Discord-Timezone", "Save-Data", "Purpose", "Sec-Purpose", } { req.Header.Del(h) } 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()) } } var htmlURLAttrDoubleRe = regexp.MustCompile(`(?i)((?:src|href|action)\s*=\s*)"((?:https?:)?//[^"]+)"`) var htmlURLAttrSingleRe = regexp.MustCompile(`(?i)((?:src|href|action)\s*=\s*)'((?:https?:)?//[^']+)'`) var ( scriptBlockRe = regexp.MustCompile(`(?is)]*>.*?`) styleBlockRe = regexp.MustCompile(`(?is)]*>.*?`) ) // rewriteHTMLAttrsServerSide rewrites absolute and protocol-relative URLs in // src/href/action attributes of raw HTML. URLs matching the upstream origin go // to localhost; other absolute URLs are routed through /generic_proxy. Skips // `, localOrigin, upstreamOrigin) // Inject as early as possible — at the opening tag — so XHR/fetch // overrides are active before any inline