Browse Source

fix: fix invisible captcha on Windows and cross-origin resource loading

pull/162/head
Moroka8 2 months ago
parent
commit
6dfa342bf2
  1. 39
      client/main.go
  2. 91
      client/manual_captcha.go

39
client/main.go

@ -328,7 +328,7 @@ func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
return nil
}
// Extract session token if redirect_uri present
// Extract session token
var sessionToken string
if RedirectURI != "" {
if parsed, err := neturl.Parse(RedirectURI); err == nil {
@ -338,6 +338,12 @@ func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError {
return nil
}
}
// Fallback to top-level session_token field if not in redirect_uri
if sessionToken == "" {
if st, ok := errData["session_token"].(string); ok {
sessionToken = st
}
}
// Extract is_sound_captcha_available
isSound, ok := errData["is_sound_captcha_available"].(bool)
@ -1783,7 +1789,31 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne
}
}
func setupGlobalResolver() {
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
dnsServers := []string{"77.88.8.8:53", "77.88.8.1:53", "8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53", "1.0.0.1:53"}
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
var lastErr error
for _, dns := range dnsServers {
conn, err := dialer.DialContext(ctx, "udp", dns)
if err == nil {
return conn, nil
}
lastErr = err
}
return nil, lastErr
},
}
}
func main() {
setupGlobalResolver()
ctx, cancel := context.WithCancel(context.Background())
globalAppCancel = cancel
defer cancel()
@ -2047,12 +2077,15 @@ func runVLESSMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listen
if err != nil {
log.Panicf("TCP listen: %s", err)
}
context.AfterFunc(ctx, func() { _ = listener.Close() })
wrappedListener := listener
context.AfterFunc(ctx, func() { _ = wrappedListener.Close() })
log.Printf("VLESS mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions)
var wgConn sync.WaitGroup
for {
tcpConn, err := listener.Accept()
tcpConn, err := wrappedListener.Accept()
if err != nil {
select {
case <-ctx.Done():

91
client/manual_captcha.go

@ -14,6 +14,7 @@ import (
"net/http/httputil"
neturl "net/url"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
@ -164,11 +165,68 @@ func rewriteProxyCookies(header http.Header) {
}
}
// 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?:)?//[^']+)'`)
// 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)
}
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]) + `'`
})
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 <script src> / <link href> / <img src>
// resources immediately as it parses HTML — before any injected JS can intercept them.
html = rewriteHTMLAttrsServerSide(html, targetURL)
script := fmt.Sprintf(`
<script>
(function() {
@ -325,7 +383,11 @@ func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string {
</script>
`, localOrigin, upstreamOrigin)
// Step 3: inject the client-side script as early as possible — at the opening <head> tag
// so that XHR/fetch overrides are active before any inline <script> block in <head> runs.
switch {
case strings.Contains(html, "<head>"):
return strings.Replace(html, "<head>", "<head>"+script, 1)
case strings.Contains(html, "</head>"):
return strings.Replace(html, "</head>", script+"</head>", 1)
case strings.Contains(html, "</body>"):
@ -389,6 +451,7 @@ func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-ch
fmt.Println("==============================================")
fmt.Println()
log.Printf("[%s] Opening browser: %s", logPrefix, captchaURL)
openBrowser(captchaURL)
key := <-keyCh
@ -564,6 +627,26 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string,
req.Out.URL.RawQuery = targetParsed.RawQuery
rewriteProxyRequest(req.Out, targetParsed)
},
ModifyResponse: func(res *http.Response) error {
// Strip security headers that can block cross-origin resource loading
// when static assets (JS/CSS) are served through the proxy.
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",
} {
res.Header.Del(h)
}
// Allow the browser to use the resource cross-origin
res.Header.Set("Access-Control-Allow-Origin", "*")
return nil
},
}
genericReverse.ServeHTTP(w, r)
})
@ -592,7 +675,13 @@ func openBrowser(url string) {
func browserOpenCommands(goos string, url string) []browserCommand {
switch goos {
case "windows":
return []browserCommand{{name: "cmd", args: []string{"/c", "start", url}}}
// 'rundll32 url.dll,FileProtocolHandler' is more reliable than 'cmd /c start'
// because it doesn't involve the shell (cmd.exe), avoiding issues with '&' and other special characters.
return []browserCommand{
{name: "rundll32", args: []string{"url.dll,FileProtocolHandler", url}},
// Fallback with empty title argument for 'start' to handle potential quoting issues
{name: "cmd", args: []string{"/c", "start", "", url}},
}
case "darwin":
return []browserCommand{{name: "open", args: []string{url}}}
case "linux":

Loading…
Cancel
Save