Passing CloudFlare checks

This commit is contained in:
Koitharu
2020-11-09 19:17:00 +02:00
parent 17c20b2bf9
commit 5190ec3e98
7 changed files with 186 additions and 10 deletions

View File

@@ -7,18 +7,22 @@ import java.util.*
class UserAgentInterceptor : Interceptor {
private val userAgent = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language
)
override fun intercept(chain: Interceptor.Chain) = chain.proceed(
chain.request().newBuilder()
.header("User-Agent", userAgent)
.build()
)
companion object {
val userAgent
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language
)
}
}

View File

@@ -16,6 +16,7 @@ import kotlinx.android.synthetic.main.fragment_list.*
import moxy.MvpDelegate
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaTag
@@ -32,6 +33,7 @@ import org.koitharu.kotatsu.ui.base.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.ui.list.filter.FilterAdapter
import org.koitharu.kotatsu.ui.list.filter.OnFilterChangedListener
import org.koitharu.kotatsu.ui.utils.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.utils.UiUtils
import org.koitharu.kotatsu.utils.ext.*
@@ -166,6 +168,9 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
}
override fun onListError(e: Throwable) {
if (e is CloudFlareProtectedException) {
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
}
if (recyclerView.hasItems) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
.show()

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.ui.utils.cloudflare
interface CloudFlareCallback {
fun onPageLoaded()
fun onCheckPassed()
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.ui.utils.cloudflare
import android.graphics.Bitmap
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class CloudFlareClient(
private val callback: CloudFlareCallback,
private val targetUrl: String
) : WebViewClient(), KoinComponent {
private val cookieJar = get<CookieJar>()
private val cookieManager = CookieManager.getInstance()
init {
cookieManager.removeAllCookies(null)
}
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
checkClearance()
}
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
callback.onPageLoaded()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
callback.onPageLoaded()
}
private fun checkClearance() {
val httpUrl = targetUrl.toHttpUrl()
val cookies = cookieManager.getCookie(targetUrl).split(';').mapNotNull {
Cookie.parse(httpUrl, it)
}
if (cookies.none { it.name == CF_CLEARANCE }) {
return
}
cookieJar.saveFromResponse(httpUrl, cookies)
callback.onCheckPassed()
}
private companion object {
const val CF_UID = "__cfduid"
const val CF_CLEARANCE = "cf_clearance"
}
}

View File

@@ -0,0 +1,78 @@
package org.koitharu.kotatsu.ui.utils.cloudflare
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.android.synthetic.main.fragment_cloudflare.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.ui.base.AlertDialogFragment
import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs
class CloudFlareDialog : AlertDialogFragment(R.layout.fragment_cloudflare), CloudFlareCallback {
private val url by stringArgument(ARG_URL)
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = UserAgentInterceptor.userAgent
}
webView.webViewClient = CloudFlareClient(this, url.orEmpty())
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
if (url.isNullOrEmpty()) {
dismissAllowingStateLoss()
} else {
webView.loadUrl(url.orEmpty())
}
}
override fun onDestroyView() {
webView.stopLoading()
super.onDestroyView()
}
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setNegativeButton(android.R.string.cancel, null)
}
override fun onResume() {
super.onResume()
webView.onResume()
}
override fun onPause() {
webView.onPause()
super.onPause()
}
override fun onPageLoaded() {
progressBar?.isInvisible = true
}
override fun onCheckPassed() {
((parentFragment ?: activity) as? SwipeRefreshLayout.OnRefreshListener)?.onRefresh()
dismiss()
}
companion object {
const val TAG = "CloudFlareDialog"
private const val ARG_URL = "url"
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
putString(ARG_URL, url)
}
}
}

View File

@@ -33,7 +33,7 @@ object CacheUtils {
fun createHttpCache(context: Context): Cache {
val directory = (context.externalCacheDir ?: context.cacheDir).sub("http")
directory.mkdirs()
val maxSize = calculateDiskCacheSize(directory)
val maxSize = calculateDiskCacheSize(directory) // TODO blocking call
return Cache(directory, maxSize)
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.progressindicator.ProgressIndicator
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="40dp"
android:indeterminate="true"
app:indicatorColor="?colorAccent"
app:indicatorType="circular" />
</FrameLayout>