package vk import ( "bytes" "compress/gzip" "context" "encoding/json" "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "strings" "sync" "time" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var activeCaptchaProxy struct { sync.Mutex listener net.Listener port int keyCh chan string doneCh chan struct{} } func StartCaptchaProxy(redirectURI string, dialer N.Dialer) int { StopCaptchaProxy() targetURL, err := url.Parse(redirectURI) if err != nil { return 0 } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return 0 } port := listener.Addr().(*net.TCPAddr).Port localOrigin := fmt.Sprintf("http://127.0.0.1:%d", port) upstreamOrigin := targetURL.Scheme + "://" + targetURL.Host keyCh := make(chan string, 1) transport := &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ForceAttemptHTTP2: false, } if dialer != nil { transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) } } proxy := &httputil.ReverseProxy{ Transport: transport, Rewrite: func(req *httputil.ProxyRequest) { req.Out.URL.Scheme = targetURL.Scheme req.Out.URL.Host = targetURL.Host if req.Out.URL.Path == "" { req.Out.URL.Path = targetURL.Path } req.Out.Host = targetURL.Host req.Out.Header.Del("Accept-Encoding") req.Out.Header.Del("TE") for _, headerName := range []string{"Origin", "Referer"} { val := req.Out.Header.Get(headerName) if val != "" { req.Out.Header.Set(headerName, strings.ReplaceAll(val, localOrigin, upstreamOrigin)) } } }, ModifyResponse: func(res *http.Response) error { rewriteProxyCookies(res) if res.StatusCode >= 300 && res.StatusCode < 400 { if loc := res.Header.Get("Location"); loc != "" { res.Header.Set("Location", strings.ReplaceAll(loc, upstreamOrigin, localOrigin)) } } contentType := res.Header.Get("Content-Type") shouldInspect := isHTMLLike(contentType) || strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") if !shouldInspect { return nil } reader := res.Body decompressed := false if res.Header.Get("Content-Encoding") == "gzip" { gzReader, err := gzip.NewReader(res.Body) if err == nil { reader = gzReader decompressed = true defer gzReader.Close() } } bodyBytes, err := io.ReadAll(reader) if err != nil { return err } res.Body.Close() if strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") { token := extractSuccessToken(bodyBytes) if token != "" { select { case keyCh <- token: default: } } } if isHTMLLike(contentType) { for _, h := range []string{ "Content-Security-Policy", "Content-Security-Policy-Report-Only", "X-Content-Security-Policy", "X-WebKit-CSP", "Cross-Origin-Opener-Policy", "Cross-Origin-Embedder-Policy", "Cross-Origin-Resource-Policy", "X-Frame-Options", "Strict-Transport-Security", "Alt-Svc", } { res.Header.Del(h) } bodyBytes = []byte(rewriteCaptchaHTML(string(bodyBytes), localOrigin, upstreamOrigin)) } if decompressed { res.Header.Del("Content-Encoding") } res.Body = io.NopCloser(bytes.NewReader(bodyBytes)) res.ContentLength = int64(len(bodyBytes)) res.Header.Set("Content-Length", fmt.Sprint(len(bodyBytes))) return nil }, } mux := http.NewServeMux() mux.HandleFunc("/local-captcha-result", func(w http.ResponseWriter, r *http.Request) { token := r.FormValue("token") if token != "" { select { case keyCh <- token: default: } } w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprint(w, "ok") }) mux.HandleFunc("/generic_proxy", func(w http.ResponseWriter, r *http.Request) { proxyURL := r.URL.Query().Get("proxy_url") parsed, err := url.Parse(proxyURL) if err != nil || parsed.Host == "" { http.Error(w, "Bad URL", http.StatusBadRequest) return } genericProxy := &httputil.ReverseProxy{ Transport: transport, Rewrite: func(req *httputil.ProxyRequest) { req.Out.URL.Scheme = parsed.Scheme req.Out.URL.Host = parsed.Host req.Out.URL.Path = parsed.Path req.Out.URL.RawQuery = parsed.RawQuery req.Out.Host = parsed.Host req.Out.Header.Del("Accept-Encoding") }, } genericProxy.ServeHTTP(w, r) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" { localPath := targetURL.Path if targetURL.RawQuery != "" { localPath += "?" + targetURL.RawQuery } http.Redirect(w, r, localPath, http.StatusTemporaryRedirect) return } proxy.ServeHTTP(w, r) }) activeCaptchaProxy.Lock() activeCaptchaProxy.listener = listener activeCaptchaProxy.port = port activeCaptchaProxy.keyCh = keyCh activeCaptchaProxy.doneCh = make(chan struct{}) activeCaptchaProxy.Unlock() go http.Serve(listener, mux) return port } func GetCaptchaResult() string { activeCaptchaProxy.Lock() ch := activeCaptchaProxy.keyCh done := activeCaptchaProxy.doneCh activeCaptchaProxy.Unlock() if ch == nil || done == nil { return "" } select { case token := <-ch: return token case <-done: return "" case <-time.After(300 * time.Second): return "" } } func StopCaptchaProxy() { activeCaptchaProxy.Lock() ln := activeCaptchaProxy.listener done := activeCaptchaProxy.doneCh activeCaptchaProxy.listener = nil activeCaptchaProxy.port = 0 activeCaptchaProxy.keyCh = nil activeCaptchaProxy.doneCh = nil activeCaptchaProxy.Unlock() if done != nil { close(done) } if ln != nil { ln.Close() } } func rewriteProxyCookies(res *http.Response) { cookies := res.Cookies() if len(cookies) == 0 { return } res.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 } res.Header.Add("Set-Cookie", cookie.String()) } } func isHTMLLike(contentType string) bool { return strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml") } 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 rewriteCaptchaHTML(html, localOrigin, upstreamOrigin string) string { html = strings.ReplaceAll(html, upstreamOrigin, localOrigin) script := fmt.Sprintf(` `, localOrigin, upstreamOrigin) if idx := strings.Index(html, ""); idx >= 0 { return html[:idx] + script + html[idx:] } if idx := strings.Index(html, ""); idx >= 0 { return html[:idx] + script + html[idx:] } return html + script }