Use OkHttp to solve the challenge
This commit is contained in:
parent
f1f6a2b341
commit
ecc1520100
1 changed files with 73 additions and 43 deletions
|
@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.Looper
|
||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
|
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.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import timber.log.Timber
|
|
||||||
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
|
||||||
|
@ -19,30 +20,33 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
|
|
||||||
private val handler by lazy {
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
val thread = HandlerThread("WebViewThread").apply {
|
|
||||||
uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, e ->
|
/**
|
||||||
Timber.e(e)
|
* 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
|
||||||
|
* Application class.
|
||||||
|
*/
|
||||||
|
private val initWebView by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT >= 17) {
|
||||||
|
WebSettings.getDefaultUserAgent(context)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
start()
|
|
||||||
}
|
|
||||||
Handler(thread.looper)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
initWebView
|
||||||
|
|
||||||
val response = chain.proceed(chain.request())
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
// 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()
|
||||||
if (resolveWithWebView(chain.request())) {
|
val solutionRequest = resolveWithWebView(chain.request())
|
||||||
// Retry original request
|
return chain.proceed(solutionRequest)
|
||||||
return chain.proceed(chain.request())
|
|
||||||
} else {
|
|
||||||
throw Exception("Failed resolving Cloudflare challenge")
|
|
||||||
}
|
|
||||||
} 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,45 +57,55 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isChallengeResolverUrl(url: String): Boolean {
|
private fun isChallengeSolutionUrl(url: String): Boolean {
|
||||||
return "chk_jschl" in url
|
return "chk_jschl" in url
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private fun resolveWithWebView(request: Request): Boolean {
|
private fun resolveWithWebView(request: Request): Request {
|
||||||
|
// 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)
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
var result = false
|
var webView: WebView? = null
|
||||||
var isResolvingChallenge = false
|
var solutionUrl: String? = null
|
||||||
|
var challengeFound = false
|
||||||
|
|
||||||
val requestUrl = 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) ?: "" }
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
val view = WebView(context)
|
val view = WebView(context)
|
||||||
|
webView = view
|
||||||
view.settings.javaScriptEnabled = true
|
view.settings.javaScriptEnabled = true
|
||||||
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 {
|
||||||
|
if (isChallengeSolutionUrl(url)) {
|
||||||
|
solutionUrl = url
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
return solutionUrl != null
|
||||||
|
}
|
||||||
|
|
||||||
override fun shouldInterceptRequestCompat(
|
override fun shouldInterceptRequestCompat(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
url: String
|
url: String
|
||||||
): WebResourceResponse? {
|
): WebResourceResponse? {
|
||||||
val isChallengeResolverUrl = isChallengeResolverUrl(url)
|
if (solutionUrl != null) {
|
||||||
if (requestUrl != url && !isChallengeResolverUrl) {
|
// Intercept any request when we have the solution.
|
||||||
return WebResourceResponse("text/plain", "UTF-8", null)
|
return WebResourceResponse("text/plain", "UTF-8", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChallengeResolverUrl) {
|
|
||||||
isResolvingChallenge = true
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
super.onPageFinished(view, url)
|
if (url == origRequestUrl) {
|
||||||
if (isResolvingChallenge && url == requestUrl) {
|
// The first request didn't return the challenge, abort.
|
||||||
setResultAndFinish(true)
|
if (!challengeFound) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,27 +116,43 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
failingUrl: String,
|
failingUrl: String,
|
||||||
isMainFrame: Boolean
|
isMainFrame: Boolean
|
||||||
) {
|
) {
|
||||||
if ((errorCode != 503 && requestUrl == failingUrl) ||
|
if (isMainFrame) {
|
||||||
isChallengeResolverUrl(failingUrl)
|
if (errorCode == 503) {
|
||||||
) {
|
// Found the cloudflare challenge page.
|
||||||
setResultAndFinish(false)
|
challengeFound = true
|
||||||
}
|
} else {
|
||||||
}
|
// Unlock thread, the challenge wasn't found.
|
||||||
|
|
||||||
private fun setResultAndFinish(resolved: Boolean) {
|
|
||||||
result = resolved
|
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
view.stopLoading()
|
|
||||||
view.destroy()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Any error on the main frame that isn't the Cloudflare check should unlock
|
||||||
|
// OkHttp's thread.
|
||||||
|
if (errorCode != 503 && isMainFrame) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webView?.loadUrl(origRequestUrl, headers)
|
||||||
|
}
|
||||||
|
|
||||||
view.loadUrl(requestUrl, headers)
|
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
|
||||||
}
|
// around 4 seconds but it can take more due to slow networks or server issues.
|
||||||
|
|
||||||
latch.await(12, TimeUnit.SECONDS)
|
latch.await(12, TimeUnit.SECONDS)
|
||||||
|
|
||||||
return result
|
handler.post {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue