Fix Cloudflare Interceptor when User-Agent is Empty
This commit is contained in:
parent
29134f6bb0
commit
0d37dd9a95
1 changed files with 78 additions and 41 deletions
|
@ -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,59 +45,75 @@ 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) {
|
||||||
try {
|
return response
|
||||||
response.close()
|
|
||||||
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
|
|
||||||
throw IOException(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
try {
|
||||||
|
response.close()
|
||||||
|
networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
|
||||||
|
val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
|
||||||
|
.firstOrNull { it.name == "cf_clearance" }
|
||||||
|
resolveWithWebView(originalRequest, oldCookie)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
chain.proceed(originalRequest)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
|
// we don't crash the entire app
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
) {
|
) {
|
||||||
|
@ -105,11 +123,11 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedErrorCompat(
|
override fun onReceivedErrorCompat(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
errorCode: Int,
|
errorCode: Int,
|
||||||
description: String?,
|
description: String?,
|
||||||
failingUrl: String,
|
failingUrl: String,
|
||||||
isMainFrame: Boolean
|
isMainFrame: Boolean
|
||||||
) {
|
) {
|
||||||
if (isMainFrame) {
|
if (isMainFrame) {
|
||||||
if (errorCode == 503) {
|
if (errorCode == 503) {
|
||||||
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue