Fix CloudFlare protection detection (close #1129)

master
Koitharu 2 years ago
parent 4faef85086
commit 9ea1122ca0
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 675 versionCode = 676
versionName = '7.6.2' versionName = '7.6.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {
@ -82,7 +82,7 @@ afterEvaluate {
} }
} }
dependencies { dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:6f7e1fcfb2') { implementation('com.github.KotatsuApp:kotatsu-parsers:1ebb298cd7') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

@ -32,6 +32,7 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -175,8 +176,7 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
private suspend fun clearCfCookies(url: HttpUrl) = runInterruptible(Dispatchers.Default) { private suspend fun clearCfCookies(url: HttpUrl) = runInterruptible(Dispatchers.Default) {
cookieJar.removeCookies(url) { cookie -> cookieJar.removeCookies(url) { cookie ->
val name = cookie.name CloudFlareHelper.isCloudFlareCookie(cookie.name)
name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf") || name == "csrftoken"
} }
} }

@ -2,11 +2,10 @@ package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap import android.graphics.Bitmap
import android.webkit.WebView import android.webkit.WebView
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.browser.BrowserClient import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
private const val CF_CLEARANCE = "cf_clearance"
private const val LOOP_COUNTER = 3 private const val LOOP_COUNTER = 3
class CloudFlareClient( class CloudFlareClient(
@ -50,8 +49,5 @@ class CloudFlareClient(
} }
} }
private fun getClearance(): String? { private fun getClearance() = CloudFlareHelper.getClearanceCookie(cookieJar, targetUrl)
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
.find { it.name == CF_CLEARANCE }?.value
}
} }

@ -2,41 +2,43 @@ package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okio.IOException
import org.jsoup.Jsoup
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import java.net.HttpURLConnection.HTTP_FORBIDDEN import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
class CloudFlareInterceptor : Interceptor { class CloudFlareInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request()) val request = chain.request()
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) { val response = chain.proceed(request)
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use { return when (CloudFlareHelper.checkResponseForProtection(response)) {
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString()) CloudFlareHelper.PROTECTION_BLOCKED -> response.closeThrowing(
} ?: return response CloudFlareBlockedException(
val hasCaptcha = content.getElementById("challenge-error-title") != null
val isBlocked = content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null
if (hasCaptcha || isBlocked) {
val request = response.request
response.closeQuietly()
if (isBlocked) {
throw CloudFlareBlockedException(
url = request.url.toString(), url = request.url.toString(),
source = request.tag(MangaSource::class.java), source = request.tag(MangaSource::class.java),
),
) )
} else {
throw CloudFlareProtectedException( CloudFlareHelper.PROTECTION_CAPTCHA -> response.closeThrowing(
CloudFlareProtectedException(
url = request.url.toString(), url = request.url.toString(),
source = request.tag(MangaSource::class.java), source = request.tag(MangaSource::class.java),
headers = request.headers, headers = request.headers,
),
) )
else -> response
} }
} }
private fun Response.closeThrowing(error: IOException): Nothing {
try {
close()
} catch (e: Exception) {
error.addSuppressed(e)
} }
return response throw error
} }
} }

Loading…
Cancel
Save