diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt index 7db589a55..c93b217ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt @@ -31,17 +31,25 @@ class AndroidCookieJar : CookieJar { } } - fun remove(url: HttpUrl) { + fun remove(url: HttpUrl, cookieNames: List? = null, maxAge: Int = -1) { val urlString = url.toString() val cookies = manager.getCookie(urlString) ?: return + fun List.filterNames(): List { + return if (cookieNames != null) { + this.filter { it in cookieNames } + } else { + this + } + } + cookies.split(";") .map { it.substringBefore("=") } - .onEach { manager.setCookie(urlString, "$it=;Max-Age=-1") } + .filterNames() + .onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") } } fun removeAll() { manager.removeAllCookies {} } - } 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 ec66d2df4..bc0fb8d20 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -5,13 +5,15 @@ import android.content.Context import android.os.Build import android.os.Handler import android.os.Looper -import android.webkit.WebResourceResponse import android.webkit.WebSettings import android.webkit.WebView import eu.kanade.tachiyomi.util.WebViewClientCompat +import okhttp3.Cookie import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response +import okhttp3.HttpUrl.Companion.toHttpUrl +import uy.kohesive.injekt.injectLazy import java.io.IOException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -22,6 +24,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { private val handler = Handler(Looper.getMainLooper()) + private val networkHelper: NetworkHelper by injectLazy() + /** * When this is called, it initializes the WebView if it wasn't already. We use this to avoid * blocking the main thread too much. If used too often we could consider moving it to the @@ -35,14 +39,21 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { initWebView - val response = chain.proceed(chain.request()) + val originalRequest = chain.request() + val response = chain.proceed(originalRequest) // Check if Cloudflare anti-bot is on if (response.code == 503 && response.header("Server") in serverCheck) { try { response.close() - val solutionRequest = resolveWithWebView(chain.request()) - return chain.proceed(solutionRequest) + 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 @@ -53,19 +64,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { return response } - private fun isChallengeSolutionUrl(url: String): Boolean { - return "chk_jschl" in url - } - @SuppressLint("SetJavaScriptEnabled") - private fun resolveWithWebView(request: Request): Request { + private fun resolveWithWebView(request: Request, oldCookie: Cookie?): Boolean { // 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 solutionUrl: String? = null var challengeFound = false + var cloudflareBypassed = false val origRequestUrl = request.url.toString() val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } @@ -77,26 +84,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { view.settings.userAgentString = request.header("User-Agent") view.webViewClient = object : WebViewClientCompat() { - override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { - if (isChallengeSolutionUrl(url)) { - solutionUrl = url + override fun onPageFinished(view: WebView, url: String) { + fun isCloudFlareBypassed(): Boolean { + return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) + .firstOrNull { it.name == "cf_clearance" } + .let { it != null && it != oldCookie } + } + + if (isCloudFlareBypassed()) { + cloudflareBypassed = true latch.countDown() } - return solutionUrl != null - } - - override fun shouldInterceptRequestCompat( - view: WebView, - url: String - ): WebResourceResponse? { - if (solutionUrl != null) { - // Intercept any request when we have the solution. - return WebResourceResponse("text/plain", "UTF-8", null) - } - return null - } - - override fun onPageFinished(view: WebView, url: String) { // Http error codes are only received since M if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && url == origRequestUrl && !challengeFound @@ -135,16 +133,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { webView?.stopLoading() webView?.destroy() } - - val solution = solutionUrl ?: throw Exception("Challenge not found") - - return Request.Builder().get() - .url(solution) - .headers(request.headers) - .addHeader("Referer", origRequestUrl) - .addHeader("Accept", "text/html,application/xhtml+xml,application/xml") - .addHeader("Accept-Language", "en") - .build() + return cloudflareBypassed } }