Browse Source

fix: improve manual captcha reliability on iOS/iSH

Implement navigator.sendBeacon for more reliable token delivery.
Add server-side success_token extraction in generic_proxy.
Make HTTP server shutdown non-fatal on iSH (ignores context deadline exceeded during cleanup)
Increase manual captcha timeout to 3 minutes using an independent context
Increase global DNS resolver timeout from 5s to 10s to improve stability
pull/162/head
Moroka8 2 months ago
parent
commit
405bdcb9a3
  1. 24
      client/main.go
  2. 85
      client/manual_captcha.go

24
client/main.go

@ -962,7 +962,9 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
}
case captchaSolveModeManual:
log.Printf("[STREAM %d] [Captcha] Triggering manual captcha fallback...", streamID)
manualCtx, manualCancel := context.WithTimeout(ctx, 60*time.Second)
// Use context.Background() so that a short deadline on the parent ctx
// (e.g. the overall auth timeout) doesn't cut the user's solve time short.
manualCtx, manualCancel := context.WithTimeout(context.Background(), 3*time.Minute)
type manualRes struct {
token string
@ -989,8 +991,24 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede
successToken = res.token
captchaKey = res.key
solveErr = res.err
// Token may be present even when err != nil (e.g. srv.Shutdown
// timed out on iSH after the token was already received).
// Treat a non-empty token as success regardless of the error.
if successToken != "" || captchaKey != "" {
if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] Token received (ignoring cleanup error: %v)", streamID, solveErr)
solveErr = nil
}
log.Printf("[STREAM %d] [Captcha] Successfully got token from browser", streamID)
} else if solveErr != nil {
log.Printf("[STREAM %d] [Captcha] solveCaptchaViaProxy returned error: %v", streamID, solveErr)
}
case <-manualCtx.Done():
solveErr = fmt.Errorf("manual captcha timed out after 60s")
if manualCtx.Err() == context.DeadlineExceeded {
solveErr = fmt.Errorf("manual captcha timed out after 3m")
} else {
solveErr = fmt.Errorf("manual captcha interrupted: %w", manualCtx.Err())
}
}
manualCancel()
}
@ -1791,7 +1809,7 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne
func setupGlobalResolver() {
dialer := &net.Dialer{
Timeout: 5 * time.Second,
Timeout: 10 * 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"}

85
client/manual_captcha.go

@ -265,14 +265,52 @@ func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string {
function handleSuccessToken(token) {
if (!token) return;
console.log('Captcha solved, sending token to proxy...');
var body = 'token=' + encodeURIComponent(token);
// sendBeacon is the most reliable on mobile Safari:
// it's fire-and-forget and works even if the page navigates away.
if (navigator && navigator.sendBeacon) {
var blob = new Blob([body], {type: 'application/x-www-form-urlencoded'});
var sent = navigator.sendBeacon('/local-captcha-result', blob);
if (sent) {
console.log('Token sent via sendBeacon');
showDone();
return;
}
}
// Fallback: fetch
fetch('/local-captcha-result', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'token=' + encodeURIComponent(token)
}).then(function() {
document.body.innerHTML = '<h2 style="text-align:center;margin-top:20vh">Done! You can close the page.</h2>';
setTimeout(function() { window.close(); }, 300);
}).catch(function() {});
body: body
}).then(function(r) {
console.log('Proxy acknowledged token');
showDone();
}).catch(function(e) {
console.error('Fetch failed, trying form submit...', e);
// Last resort: form POST (navigates the page)
var form = document.createElement('form');
form.method = 'POST';
form.action = '/local-captcha-result';
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'token';
input.value = token;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
});
}
function showDone() {
document.body.innerHTML = '<div style="text-align:center;margin-top:20vh;font-family:sans-serif">' +
'<h2 style="color:#4caf50"> Done!</h2>' +
'<p>Captcha solved successfully. You can close this tab now.</p>' +
'</div>';
// On iOS, window.close() often doesn't work, so we just let the user know they are done.
setTimeout(function() { window.close(); }, 1000);
}
var origOpen = XMLHttpRequest.prototype.open;
@ -462,10 +500,14 @@ func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-ch
key := <-keyCh
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
return "", err
// Best-effort shutdown: the token is already received, so even if
// Shutdown times out (e.g. because ishConn.SetDeadline is a no-op
// on iSH and active connections can't be force-closed), we still
// return the token successfully.
shutCtx, shutCancel := context.WithTimeout(context.Background(), 3*time.Second)
defer shutCancel()
if err := srv.Shutdown(shutCtx); err != nil {
log.Printf("%s: shutdown warning (token already received): %v", logPrefix, err)
}
return key, nil
@ -614,8 +656,15 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string,
mux := http.NewServeMux()
mux.HandleFunc("/local-captcha-result", func(w http.ResponseWriter, r *http.Request) {
notifyKey(keyCh, r.FormValue("token")) // r.FormValue automatically parses the form
token := r.FormValue("token")
if token != "" {
log.Printf("[Captcha] Received success token from browser (%d bytes)", len(token))
notifyKey(keyCh, token)
} else {
log.Printf("[Captcha] Received empty token from browser")
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "text/plain")
_, _ = fmt.Fprint(w, "ok")
})
@ -651,6 +700,22 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string,
}
// Allow the browser to use the resource cross-origin
res.Header.Set("Access-Control-Allow-Origin", "*")
// captchaNotRobot.check goes to api.vk.ru (different host from the main
// proxy upstream vk.com), so it is routed through /generic_proxy.
// Extract the success_token here so the server path works on iOS
// even if the browser-side JS callback never fires.
if strings.Contains(targetAuthURL, "captchaNotRobot.check") {
bodyBytes, readErr := io.ReadAll(res.Body)
if readErr == nil {
_ = res.Body.Close()
res.Body = io.NopCloser(bytes.NewReader(bodyBytes))
res.ContentLength = int64(len(bodyBytes))
res.Header.Set("Content-Length", fmt.Sprint(len(bodyBytes)))
notifyKey(keyCh, extractSuccessToken(bodyBytes))
}
}
return nil
},
}

Loading…
Cancel
Save