CloudflareInterceptor update (#2537)

* CloudflareInterceptor update

* Changes

* Max-Age

* Tweaks
This commit is contained in:
Mike 2020-01-25 16:37:59 -05:00 committed by arkon
parent f966187ea4
commit dcd3c709fe
2 changed files with 38 additions and 41 deletions

View file

@ -31,17 +31,25 @@ class AndroidCookieJar : CookieJar {
}
}
fun remove(url: HttpUrl) {
fun remove(url: HttpUrl, cookieNames: List<String>? = null, maxAge: Int = -1) {
val urlString = url.toString()
val cookies = manager.getCookie(urlString) ?: return
fun List<String>.filterNames(): List<String> {
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 {}
}
}

View file

@ -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
}
}