diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index a8fd4a2e18..59d77cf2ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -7,8 +7,11 @@ import android.os.Handler import android.os.Looper import android.webkit.WebSettings import android.webkit.WebView +import android.widget.Toast +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.WebViewClientCompat -import eu.kanade.tachiyomi.util.system.checkVersion +import eu.kanade.tachiyomi.util.system.isOutdated +import eu.kanade.tachiyomi.util.system.toast import okhttp3.Cookie import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor @@ -21,8 +24,6 @@ import java.util.concurrent.TimeUnit class CloudflareInterceptor(private val context: Context) : Interceptor { - private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") - private val handler = Handler(Looper.getMainLooper()) private val networkHelper: NetworkHelper by injectLazy() @@ -44,36 +45,35 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { val response = chain.proceed(originalRequest) // Check if Cloudflare anti-bot is on - if (response.code == 503 && response.header("Server") in serverCheck) { - try { - response.close() - networkHelper.cookieManager.remove(originalRequest.url, listOf("__cfduid", "cf_clearance"), 0) - val oldCookie = networkHelper.cookieManager.get(originalRequest.url) - .firstOrNull { it.name == "cf_clearance" } - return if (resolveWithWebView(originalRequest, oldCookie)) { - chain.proceed(originalRequest) - } else { - throw IOException("Failed to bypass Cloudflare!") - } - } catch (e: Exception) { - // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that - // we don't crash the entire app - throw IOException(e) - } + if (response.code != 503 || response.header("Server") !in SERVER_CHECK) { + return response } - return response + try { + response.close() + networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0) + val oldCookie = networkHelper.cookieManager.get(originalRequest.url) + .firstOrNull { it.name == "cf_clearance" } + resolveWithWebView(originalRequest, oldCookie) + return chain.proceed(originalRequest) + } catch (e: Exception) { + // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that + // we don't crash the entire app + throw IOException(e) + } } @SuppressLint("SetJavaScriptEnabled") - private fun resolveWithWebView(request: Request, oldCookie: Cookie?): Boolean { + private fun resolveWithWebView(request: Request, oldCookie: Cookie?) { // We need to lock this thread until the WebView finds the challenge solution url, because // OkHttp doesn't support asynchronous interceptors. val latch = CountDownLatch(1) var webView: WebView? = null + var challengeFound = false var cloudflareBypassed = false + var isWebviewOutdated = false val origRequestUrl = request.url.toString() val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } @@ -82,8 +82,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { val webview = WebView(context) webView = webview - webview.checkVersion() - webview.settings.javaScriptEnabled = true webview.settings.userAgentString = request.header("User-Agent") @@ -135,10 +133,28 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { latch.await(12, TimeUnit.SECONDS) handler.post { + if (!cloudflareBypassed) { + isWebviewOutdated = webView?.isOutdated() == true + } + webView?.stopLoading() webView?.destroy() } - return cloudflareBypassed + + // Throw exception if we failed to bypass Cloudflare + if (!cloudflareBypassed) { + // Prompt user to update WebView if it seems too outdated + if (isWebviewOutdated) { + context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG) + } + + throw Exception(context.getString(R.string.information_cloudflare_bypass_failure)) + } + } + + companion object { + private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") + private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance") } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index c63e079bd8..898a1260e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -15,7 +15,10 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import eu.kanade.tachiyomi.util.system.* +import eu.kanade.tachiyomi.util.system.WebViewClientCompat +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.openInBrowser +import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.invisible import eu.kanade.tachiyomi.util.view.visible import kotlinx.android.synthetic.main.webview_activity.* @@ -53,8 +56,6 @@ class WebViewActivity : BaseActivity() { val url = intent.extras!!.getString(URL_KEY) ?: return val headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } - webview.checkVersion() - webview.settings.javaScriptEnabled = true webview.settings.userAgentString = source.headers["User-Agent"] diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebviewUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebviewUtil.kt index 1d9da89963..83e864a05a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebviewUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebviewUtil.kt @@ -1,19 +1,15 @@ package eu.kanade.tachiyomi.util.system import android.webkit.WebView -import android.widget.Toast -import eu.kanade.tachiyomi.R private val WEBVIEW_UA_VERSION_REGEX by lazy { Regex(""".*Chrome/(\d+)\..*""") } -private const val MINIMUM_WEBVIEW_VERSION = 72 +private const val MINIMUM_WEBVIEW_VERSION = 70 -fun WebView.checkVersion() { - if (getWebviewMajorVersion(this) < MINIMUM_WEBVIEW_VERSION) { - this.context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG) - } +fun WebView.isOutdated(): Boolean { + return getWebviewMajorVersion(this) < MINIMUM_WEBVIEW_VERSION } // Based on https://stackoverflow.com/a/29218966 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f033e5e590..f9c73b03ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -521,6 +521,7 @@ No recently read manga Your library is empty, add series to your library from the catalogues. You have no categories. Hit the plus button to create one for organizing your library. + Failed to bypass Cloudflare Please update the WebView app for better compatibility