Fix Cloudflare Interceptor when User-Agent is Empty

This commit is contained in:
Thiago França da Silva 2020-03-08 23:34:57 -03:00 committed by Jay
parent 29134f6bb0
commit 0d37dd9a95

View file

@ -7,21 +7,23 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView 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.WebViewClientCompat
import okhttp3.Cookie import eu.kanade.tachiyomi.util.system.isOutdated
import okhttp3.Interceptor import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Request
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
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class CloudflareInterceptor(private val context: Context) : Interceptor { class CloudflareInterceptor(private val context: Context) : Interceptor {
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val networkHelper: NetworkHelper by injectLazy() private val networkHelper: NetworkHelper by injectLazy()
@ -43,16 +45,28 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
val response = chain.proceed(originalRequest) 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 SERVER_CHECK) {
return response
}
try { try {
response.close() response.close()
networkHelper.cookieManager.remove(originalRequest.url, listOf("__cfduid", "cf_clearance"), 0) networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(originalRequest.url) val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
return if (resolveWithWebView(originalRequest, oldCookie)) { resolveWithWebView(originalRequest, oldCookie)
chain.proceed(originalRequest)
// Avoid use empty User-Agent
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent",
DEFAULT_USERAGENT)
.build()
chain.proceed(newRequest)
} else { } else {
throw IOException("Failed to bypass Cloudflare!") chain.proceed(originalRequest)
} }
} 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
@ -61,41 +75,45 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
} }
return response
}
@SuppressLint("SetJavaScriptEnabled") @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 // 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 challengeFound = false var challengeFound = false
var cloudflareBypassed = false var cloudflareBypassed = false
var isWebviewOutdated = 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) ?: "" }
val withUserAgent = request.header("User-Agent").isNullOrEmpty()
handler.post { handler.post {
val view = WebView(context.applicationContext) val webview = WebView(context)
webView = view webView = webview
view.settings.javaScriptEnabled = true webview.settings.javaScriptEnabled = true
view.settings.userAgentString = request.header("User-Agent")
view.webViewClient = object : WebViewClientCompat() {
// Avoid set empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent")
?: DEFAULT_USERAGENT
webview.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean { fun isCloudFlareBypassed(): Boolean {
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
.let { it != null && it != oldCookie } .let { it != null && (it != oldCookie || withUserAgent) }
} }
if (isCloudFlareBypassed()) { if (isCloudFlareBypassed()) {
cloudflareBypassed = true cloudflareBypassed = true
latch.countDown() latch.countDown()
} }
// 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
) { ) {
@ -122,6 +140,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
} }
} }
webView?.loadUrl(origRequestUrl, headers) webView?.loadUrl(origRequestUrl, headers)
} }
@ -130,10 +149,28 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
latch.await(12, TimeUnit.SECONDS) latch.await(12, TimeUnit.SECONDS)
handler.post { handler.post {
if (!cloudflareBypassed) {
isWebviewOutdated = webView?.isOutdated() == true
}
webView?.stopLoading() webView?.stopLoading()
webView?.destroy() 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")
private const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
}
} }