Passing CloudFlare checks
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.ui.utils.cloudflare
|
||||
|
||||
interface CloudFlareCallback {
|
||||
|
||||
fun onPageLoaded()
|
||||
|
||||
fun onCheckPassed()
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
24
app/src/main/res/layout/fragment_cloudflare.xml
Normal file
24
app/src/main/res/layout/fragment_cloudflare.xml
Normal 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>
|
||||
Reference in New Issue
Block a user