From 5190ec3e985e9691210592a40d0bae5d0c5d342f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 9 Nov 2020 19:17:00 +0200 Subject: [PATCH] Passing CloudFlare checks --- .../core/network/UserAgentInterceptor.kt | 22 +++--- .../kotatsu/ui/list/MangaListFragment.kt | 5 ++ .../ui/utils/cloudflare/CloudFlareCallback.kt | 8 ++ .../ui/utils/cloudflare/CloudFlareClient.kt | 57 ++++++++++++++ .../ui/utils/cloudflare/CloudFlareDialog.kt | 78 +++++++++++++++++++ .../org/koitharu/kotatsu/utils/CacheUtils.kt | 2 +- .../main/res/layout/fragment_cloudflare.xml | 24 ++++++ 7 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareCallback.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareClient.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareDialog.kt create mode 100644 app/src/main/res/layout/fragment_cloudflare.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/UserAgentInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/UserAgentInterceptor.kt index 3ea21ce2f..626c06c60 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/UserAgentInterceptor.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/UserAgentInterceptor.kt @@ -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 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt index 3845b9cd7..3a314f37d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt @@ -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 : 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() diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareCallback.kt b/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareCallback.kt new file mode 100644 index 000000000..6f3c2514d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareCallback.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.ui.utils.cloudflare + +interface CloudFlareCallback { + + fun onPageLoaded() + + fun onCheckPassed() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareClient.kt b/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareClient.kt new file mode 100644 index 000000000..be484f6ce --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareClient.kt @@ -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() + 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareDialog.kt b/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareDialog.kt new file mode 100644 index 000000000..0e7d6a6a4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/utils/cloudflare/CloudFlareDialog.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/CacheUtils.kt b/app/src/main/java/org/koitharu/kotatsu/utils/CacheUtils.kt index 420d92bde..a1cd681b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/CacheUtils.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/CacheUtils.kt @@ -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) } diff --git a/app/src/main/res/layout/fragment_cloudflare.xml b/app/src/main/res/layout/fragment_cloudflare.xml new file mode 100644 index 000000000..38289e9bf --- /dev/null +++ b/app/src/main/res/layout/fragment_cloudflare.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file