MangaFire: prevent crash on main thread due to network issues (#11221)

* webview

* fix illegalstateexception

* fix

* lint

* timeout
This commit is contained in:
AwkwardPeak7 2025-10-25 13:22:38 +05:00 committed by Draff
parent 7e5b58bb5c
commit b4b8bbe748
Signed by: Draff
GPG Key ID: E8A89F3211677653
2 changed files with 136 additions and 84 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MangaFire' extName = 'MangaFire'
extClass = '.MangaFireFactory' extClass = '.MangaFireFactory'
extVersionCode = 14 extVersionCode = 15
isNsfw = true isNsfw = true
} }

View File

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.extension.all.mangafire
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
@ -26,6 +24,9 @@ import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse import keiyoushi.utils.tryParse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
@ -43,11 +44,12 @@ import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.seconds
class MangaFire( class MangaFire(
override val lang: String, override val lang: String,
@ -265,88 +267,11 @@ class MangaFire(
// =============================== Pages ================================ // =============================== Pages ================================
@SuppressLint("SetJavaScriptEnabled")
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val document = response.asJsoup()
var ajaxUrl: String? = null val ajaxUrl = runBlocking { getVrfFromWebview(document) }
val context = Injekt.get<Application>() return client.newCall(GET(ajaxUrl, headers)).execute()
val handler = Handler(Looper.getMainLooper())
val latch = CountDownLatch(1)
val emptyWebViewResponse = WebResourceResponse("text/html", "utf-8", Buffer().inputStream())
var webView: WebView? = null
handler.post {
val webview = WebView(context)
.also { webView = it }
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
blockNetworkImage = true
}
webview.webViewClient = object : WebViewClient() {
private val ajaxCalls = setOf("ajax/read/chapter", "ajax/read/volume")
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
val url = request.url
// allow script from their cdn
if (url.host.orEmpty().contains("mfcdn.cc") && url.pathSegments.lastOrNull().orEmpty().contains("js")) {
Log.d(name, "allowed: $url")
return fetchWebResource(request)
}
// allow jquery script
if (url.host.orEmpty().contains("cloudflare.com") && url.encodedPath.orEmpty().contains("jquery")) {
Log.d(name, "allowed: $url")
return fetchWebResource(request)
}
// allow ajax/read calls and intercept ajax/read/chapter or ajax/read/volume
if (url.host == "mangafire.to" && url.encodedPath.orEmpty().contains("ajax/read")) {
if (ajaxCalls.any { url.encodedPath!!.contains(it) }) {
Log.d(name, "found: $url")
if (url.getQueryParameter("vrf") != null) {
ajaxUrl = url.toString()
}
latch.countDown()
} else {
// need to allow other call to ajax/read
Log.d(name, "allowed: $url")
return fetchWebResource(request)
}
}
Log.d(name, "denied: $url")
return emptyWebViewResponse
}
}
webview.loadDataWithBaseURL(document.location(), document.outerHtml(), "text/html", "utf-8", "")
}
latch.await(20, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
}
if (latch.count == 1L) {
throw Exception("Timeout getting vrf token")
} else if (ajaxUrl == null) {
throw Exception("Unable to find vrf token")
}
return client.newCall(GET(ajaxUrl!!, headers)).execute()
.parseAs<ResponseDto<PageListDto>>().result .parseAs<ResponseDto<PageListDto>>().result
.pages.mapIndexed { index, image -> .pages.mapIndexed { index, image ->
val url = image.url val url = image.url
@ -357,6 +282,133 @@ class MangaFire(
} }
} }
@SuppressLint("SetJavaScriptEnabled")
private suspend fun getVrfFromWebview(document: Document): String = withContext(Dispatchers.Main.immediate) {
withTimeoutOrNull(20.seconds) {
suspendCancellableCoroutine { continuation ->
val emptyWebViewResponse = runCatching {
WebResourceResponse("text/html", "utf-8", Buffer().inputStream())
}.getOrElse {
continuation.resumeWithException(it)
return@suspendCancellableCoroutine
}
val context = Injekt.get<Application>()
var webview: WebView? = WebView(context)
fun cleanup() = runBlocking(Dispatchers.Main.immediate) {
webview?.stopLoading()
webview?.destroy()
webview = null
}
webview?.apply {
with(settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
blockNetworkImage = true
}
webViewClient = object : WebViewClient() {
private val ajaxCalls = setOf("ajax/read/chapter", "ajax/read/volume")
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
val url = request.url
// allow script from their cdn
if (
url.host.orEmpty().contains("mfcdn.cc") &&
url.pathSegments.lastOrNull().orEmpty().contains("js")
) {
Log.d(name, "allowed: $url")
runCatching { fetchWebResource(request) }
.onSuccess { return it }
.onFailure {
if (continuation.isActive) {
continuation.resumeWithException(it)
cleanup()
}
}
}
// allow jquery script
if (
url.host.orEmpty().contains("cloudflare.com") &&
url.encodedPath.orEmpty().contains("jquery")
) {
Log.d(name, "allowed: $url")
runCatching { fetchWebResource(request) }
.onSuccess { return it }
.onFailure {
if (continuation.isActive) {
continuation.resumeWithException(it)
cleanup()
}
}
}
// allow ajax/read calls and intercept ajax/read/chapter or ajax/read/volume
if (
url.host == "mangafire.to" &&
url.encodedPath.orEmpty().contains("ajax/read")
) {
if (ajaxCalls.any { url.encodedPath!!.contains(it) }) {
Log.d(name, "found: $url")
if (url.getQueryParameter("vrf") != null) {
if (continuation.isActive) {
continuation.resume(url.toString())
cleanup()
}
} else {
if (continuation.isActive) {
continuation.resumeWithException(
Exception("Unable to find vrf token"),
)
cleanup()
}
}
} else {
// need to allow other call to ajax/read
Log.d(name, "allowed: $url")
runCatching { fetchWebResource(request) }
.onSuccess { return it }
.onFailure {
if (continuation.isActive) {
continuation.resumeWithException(it)
cleanup()
}
}
}
}
Log.d(name, "denied: $url")
return emptyWebViewResponse
}
}
loadDataWithBaseURL(
document.location(),
document.outerHtml(),
"text/html",
"utf-8",
"",
)
}
continuation.invokeOnCancellation {
cleanup()
}
}
} ?: throw Exception("Timeout getting vrf token")
}
private fun fetchWebResource(request: WebResourceRequest): WebResourceResponse = runBlocking(Dispatchers.IO) { private fun fetchWebResource(request: WebResourceRequest): WebResourceResponse = runBlocking(Dispatchers.IO) {
val okhttpRequest = Request.Builder().apply { val okhttpRequest = Request.Builder().apply {
url(request.url.toString()) url(request.url.toString())