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 urlString = url.toString()
val cookies = manager.getCookie(urlString) ?: return 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(";") cookies.split(";")
.map { it.substringBefore("=") } .map { it.substringBefore("=") }
.onEach { manager.setCookie(urlString, "$it=;Max-Age=-1") } .filterNames()
.onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") }
} }
fun removeAll() { fun removeAll() {
manager.removeAllCookies {} manager.removeAllCookies {}
} }
} }

View File

@ -5,13 +5,15 @@ import android.content.Context
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import eu.kanade.tachiyomi.util.WebViewClientCompat import eu.kanade.tachiyomi.util.WebViewClientCompat
import okhttp3.Cookie
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.HttpUrl.Companion.toHttpUrl
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -22,6 +24,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
private val handler = Handler(Looper.getMainLooper()) 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 * 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 * 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 { override fun intercept(chain: Interceptor.Chain): Response {
initWebView initWebView
val response = chain.proceed(chain.request()) val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
if (response.code == 503 && response.header("Server") in serverCheck) { if (response.code == 503 && response.header("Server") in serverCheck) {
try { try {
response.close() response.close()
val solutionRequest = resolveWithWebView(chain.request()) networkHelper.cookieManager.remove(originalRequest.url, listOf("__cfduid", "cf_clearance"), 0)
return chain.proceed(solutionRequest) 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) { } catch (e: Exception) {
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app // we don't crash the entire app
@ -53,19 +64,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
return response return response
} }
private fun isChallengeSolutionUrl(url: String): Boolean {
return "chk_jschl" in url
}
@SuppressLint("SetJavaScriptEnabled") @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 // We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors. // OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
var webView: WebView? = null var webView: WebView? = null
var solutionUrl: String? = null
var challengeFound = false var challengeFound = false
var cloudflareBypassed = false
val origRequestUrl = request.url.toString() 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) ?: "" }
@ -77,26 +84,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
view.settings.userAgentString = request.header("User-Agent") view.settings.userAgentString = request.header("User-Agent")
view.webViewClient = object : WebViewClientCompat() { view.webViewClient = object : WebViewClientCompat() {
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { override fun onPageFinished(view: WebView, url: String) {
if (isChallengeSolutionUrl(url)) { fun isCloudFlareBypassed(): Boolean {
solutionUrl = url return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
.firstOrNull { it.name == "cf_clearance" }
.let { it != null && it != oldCookie }
}
if (isCloudFlareBypassed()) {
cloudflareBypassed = true
latch.countDown() 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 // Http error codes are only received since M
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
url == origRequestUrl && !challengeFound url == origRequestUrl && !challengeFound
@ -135,16 +133,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
webView?.stopLoading() webView?.stopLoading()
webView?.destroy() webView?.destroy()
} }
return cloudflareBypassed
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()
} }
} }