|
|
@ -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())
|
|
|
|
latch.countDown()
|
|
|
|
.firstOrNull { it.name == "cf_clearance" }
|
|
|
|
}
|
|
|
|
.let { it != null && it != oldCookie }
|
|
|
|
return solutionUrl != null
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun shouldInterceptRequestCompat(
|
|
|
|
if (isCloudFlareBypassed()) {
|
|
|
|
view: WebView,
|
|
|
|
cloudflareBypassed = true
|
|
|
|
url: String
|
|
|
|
latch.countDown()
|
|
|
|
): 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()
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|