From ac249e3668a57f24782a49218834064459ba2d09 Mon Sep 17 00:00:00 2001 From: tigeryu8900 <37685787+tigeryu8900@users.noreply.github.com> Date: Sun, 23 Aug 2026 03:50:31 -0700 Subject: [PATCH] Exit early in CloudflareInterceptor when cloudflare challenge is interactive (#3842) --- .../interceptor/CloudflareInterceptor.kt | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 517679aca..903b204ff 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network.interceptor import android.annotation.SuppressLint import android.content.Context +import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView @@ -32,18 +33,9 @@ class CloudflareInterceptor( override fun shouldIntercept(response: Response): Boolean { // Check if Cloudflare anti-bot is on - return if (response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK) { - val document = Jsoup.parse( - response.peekBody(Long.MAX_VALUE).string(), - response.request.url.toString(), - ) - - // solve with webview only on captcha, not on geo block - document.getElementById("challenge-error-title") != null || - document.getElementById("challenge-error-text") != null - } else { - false - } + // Checking the cf-mitigated header is the official way to detect a Cloudflare challenge: + // https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/ + return response.header("cf-mitigated") == "challenge" && response.header("Server") in SERVER_CHECK } override fun intercept( @@ -87,6 +79,18 @@ class CloudflareInterceptor( executor.execute { webview = createWebView(originalRequest) + webview.addJavascriptInterface( + object { + @Suppress("unused") + @JavascriptInterface + fun interactiveDetected() { + // The challenge cannot be solved non-interactively, abort. + latch.countDown() + } + }, + "mihon", + ) + webview.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { fun isCloudFlareBypassed(): Boolean { @@ -100,9 +104,23 @@ class CloudflareInterceptor( latch.countDown() } - if (url == origRequestUrl && !challengeFound) { - // The first request didn't return the challenge, abort. - latch.countDown() + if (url == origRequestUrl) { + if (!challengeFound) { + // The first request didn't return the challenge, abort. + latch.countDown() + } else { + // Listen for an interactiveBegin event + view.evaluateJavascript( + """ + addEventListener("message", ({data}) => { + if (data?.source === "cloudflare-challenge" && data?.event === "interactiveBegin") { + mihon.interactiveDetected(); + } + }) + """.trimIndent(), + null, + ) + } } } @@ -112,7 +130,7 @@ class CloudflareInterceptor( errorResponse: WebResourceResponse?, ) { if (request?.isForMainFrame == true) { - if (errorResponse?.statusCode in ERROR_CODES) { + if (errorResponse?.responseHeaders["cf-mitigated"] == "challenge") { // Found the Cloudflare challenge page. challengeFound = true } else { @@ -151,7 +169,6 @@ class CloudflareInterceptor( } } -private val ERROR_CODES = listOf(403, 503) private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val COOKIE_NAMES = listOf("cf_clearance")