Exit early in CloudflareInterceptor when cloudflare challenge is interactive (#3842)

This commit is contained in:
tigeryu8900
2026-08-23 03:50:31 -07:00
committed by GitHub
parent e3813028bd
commit ac249e3668
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
@@ -32,18 +33,9 @@ class CloudflareInterceptor(
override fun shouldIntercept(response: Response): Boolean { override fun shouldIntercept(response: Response): Boolean {
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
return if (response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK) { // Checking the cf-mitigated header is the official way to detect a Cloudflare challenge:
val document = Jsoup.parse( // https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/
response.peekBody(Long.MAX_VALUE).string(), return response.header("cf-mitigated") == "challenge" && response.header("Server") in SERVER_CHECK
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
}
} }
override fun intercept( override fun intercept(
@@ -87,6 +79,18 @@ class CloudflareInterceptor(
executor.execute { executor.execute {
webview = createWebView(originalRequest) 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() { webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean { fun isCloudFlareBypassed(): Boolean {
@@ -100,9 +104,23 @@ class CloudflareInterceptor(
latch.countDown() latch.countDown()
} }
if (url == origRequestUrl && !challengeFound) { if (url == origRequestUrl) {
// The first request didn't return the challenge, abort. if (!challengeFound) {
latch.countDown() // 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?, errorResponse: WebResourceResponse?,
) { ) {
if (request?.isForMainFrame == true) { if (request?.isForMainFrame == true) {
if (errorResponse?.statusCode in ERROR_CODES) { if (errorResponse?.responseHeaders["cf-mitigated"] == "challenge") {
// Found the Cloudflare challenge page. // Found the Cloudflare challenge page.
challengeFound = true challengeFound = true
} else { } else {
@@ -151,7 +169,6 @@ class CloudflareInterceptor(
} }
} }
private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance") private val COOKIE_NAMES = listOf("cf_clearance")