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 1aeb1c4fe9..2fe60bb098 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -10,7 +10,10 @@ import android.widget.Toast import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.system.WebViewClientCompat +import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.isOutdated +import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.toast import okhttp3.Cookie import okhttp3.HttpUrl.Companion.toHttpUrl @@ -39,9 +42,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { @Synchronized override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + if (!WebViewUtil.supportsWebView(context)) { + launchUI { + context.toast(R.string.webview_is_required, Toast.LENGTH_LONG) + } + return chain.proceed(originalRequest) + } + initWebView - val originalRequest = chain.request() val response = chain.proceed(originalRequest) // Check if Cloudflare anti-bot is on @@ -77,16 +88,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { var isWebViewOutdated = false val origRequestUrl = request.url.toString() - val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } + val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() + headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH handler.post { val webview = WebView(context) webView = webview - webview.settings.javaScriptEnabled = true + webview.setDefaultSettings() - // Avoid set empty User-Agent, Chromium WebView will reset to default if empty - webview.settings.userAgentString = - request.header("User-Agent") ?: HttpSource.DEFAULT_USERAGENT + // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty + webview.settings.userAgentString = request.header("User-Agent") + ?: HttpSource.DEFAULT_USER_AGENT webview.webViewClient = object : WebViewClientCompat() { override fun onPageFinished(view: WebView, url: String) { @@ -101,7 +113,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { latch.countDown() } - // HTTP error codes are only received since M if (url == origRequestUrl && !challengeFound) { // The first request didn't return the challenge, abort. latch.countDown() @@ -117,7 +128,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { ) { if (isMainFrame) { if (errorCode == 503) { - // Found the cloudflare challenge page. + // Found the Cloudflare challenge page. challengeFound = true } else { // Unlock thread, the challenge wasn't found. @@ -156,6 +167,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { companion object { private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") - private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance") + private val COOKIE_NAMES = listOf("cf_clearance") } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/UserAgentInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/UserAgentInterceptor.kt index a5aa3e4f51..e1669472de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/UserAgentInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/UserAgentInterceptor.kt @@ -12,7 +12,7 @@ class UserAgentInterceptor : Interceptor { val newRequest = originalRequest .newBuilder() .removeHeader("User-Agent") - .addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT) + .addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT) .build() chain.proceed(newRequest) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 9d0784056b..1ef533f810 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -74,7 +74,7 @@ abstract class HttpSource : CatalogueSource { * Headers builder for requests. Implementations can override this method for custom headers. */ protected open fun headersBuilder() = Headers.Builder().apply { - add("User-Agent", DEFAULT_USERAGENT) + add("User-Agent", DEFAULT_USER_AGENT) } /** @@ -371,6 +371,6 @@ abstract class HttpSource : CatalogueSource { override fun getFilterList() = FilterList() companion object { - const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)" + const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63" } } 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 ed6cdbdbe9..9c5cc349e0 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,8 +1,37 @@ package eu.kanade.tachiyomi.util.system +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.webkit.CookieManager import android.webkit.WebSettings import android.webkit.WebView +import timber.log.Timber +object WebViewUtil { + const val REQUESTED_WITH = "com.android.browser" + + const val MINIMUM_WEBVIEW_VERSION = 88 + + fun supportsWebView(context: Context): Boolean { + try { + // May throw android.webkit.WebViewFactory$MissingWebViewPackageException if WebView + // is not installed + CookieManager.getInstance() + } catch (e: Throwable) { + Timber.e(e) + return false + } + + return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW) + } +} + +fun WebView.isOutdated(): Boolean { + return getWebViewMajorVersion() < WebViewUtil.MINIMUM_WEBVIEW_VERSION +} + +@SuppressLint("SetJavaScriptEnabled") fun WebView.setDefaultSettings() { with(settings) { javaScriptEnabled = true @@ -15,32 +44,25 @@ fun WebView.setDefaultSettings() { } } -private val WEBVIEW_UA_VERSION_REGEX by lazy { - Regex(""".*Chrome/(\d+)\..*""") -} - -private const val MINIMUM_WEBVIEW_VERSION = 86 - -fun WebView.isOutdated(): Boolean { - return getWebviewMajorVersion(this) < MINIMUM_WEBVIEW_VERSION -} - -// Based on https://stackoverflow.com/a/29218966 -private fun getWebviewMajorVersion(webview: WebView): Int { - val originalUA: String = webview.settings.userAgentString - - // Next call to getUserAgentString() will get us the default - webview.settings.userAgentString = null - - val uaRegexMatch = WEBVIEW_UA_VERSION_REGEX.matchEntire(webview.settings.userAgentString) - val webViewVersion: Int = if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) { +private fun WebView.getWebViewMajorVersion(): Int { + val uaRegexMatch = """.*Chrome/(\d+)\..*""".toRegex().matchEntire(getDefaultUserAgentString()) + return if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) { uaRegexMatch.groupValues[1].toInt() } else { 0 } +} + +// Based on https://stackoverflow.com/a/29218966 +private fun WebView.getDefaultUserAgentString(): String { + val originalUA: String = settings.userAgentString + + // Next call to getUserAgentString() will get us the default + settings.userAgentString = null + val defaultUserAgentString = settings.userAgentString // Revert to original UA string - webview.settings.userAgentString = originalUA + settings.userAgentString = originalUA - return webViewVersion + return defaultUserAgentString } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fcfdb5c326..275d84dde3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -788,6 +788,8 @@ Failed to bypass Cloudflare Please update the WebView app for better compatibility + + WebView is required for Tachiyomi Add