Fix WebView renderer being killed by sources setting an Origin header

This commit is contained in:
AntsyLich
2026-10-18 00:46:20 +06:00
parent 83e8f917dc
commit 5f63c983da
5 changed files with 33 additions and 28 deletions
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toWebViewHeaders
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.Dispatchers
@@ -71,7 +72,7 @@ class WebViewActivity : BaseActivity() {
val source = sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource
value = withContext(Dispatchers.IO) {
try {
source?.headers?.toMultimap()?.mapValues { it.value.getOrNull(0) ?: "" }.orEmpty()
source?.headers?.toWebViewHeaders().orEmpty()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to build headers" }
emptyMap()
@@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toWebViewHeaders
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -47,7 +48,7 @@ class WebViewViewModel(
viewModelScope.launch(Dispatchers.IO) {
val source = sourceId?.let { sourceManager.get(it) as? HttpSource }
headers.value = try {
source?.headers?.toMultimap()?.mapValues { it.value.getOrNull(0) ?: "" }.orEmpty()
source?.headers?.toWebViewHeaders().orEmpty()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to build headers" }
emptyMap()
@@ -15,6 +15,7 @@ import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.toWebViewHeaders
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import logcat.LogPriority
@@ -139,7 +140,7 @@ class CloudflareInterceptor(
} else {
originalRequest.url.newBuilder().encodedPath("/").query(null).fragment(null).build().toString()
}
val headers = parseHeaders(originalRequest.headers)
val headers = originalRequest.headers.toWebViewHeaders()
executor.execute {
webview = createWebView(originalRequest)
@@ -13,12 +13,10 @@ import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import tachiyomi.i18n.MR
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -78,16 +76,6 @@ abstract class WebViewInterceptor(
return true
}
fun parseHeaders(headers: Headers): Map<String, String> {
return headers
// Keeping unsafe header makes webview throw [net::ERR_INVALID_ARGUMENT]
.filter { (name, value) ->
isRequestHeaderSafe(name, value)
}
.groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value }
.mapValues { it.value.getOrNull(0).orEmpty() }
}
fun CountDownLatch.awaitFor30Seconds() {
await(30, TimeUnit.SECONDS)
}
@@ -100,16 +88,3 @@ abstract class WebViewInterceptor(
}
}
}
// Based on [IsRequestHeaderSafe] in
// https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc
private fun isRequestHeaderSafe(_name: String, _value: String): Boolean {
val name = _name.lowercase(Locale.ENGLISH)
val value = _value.lowercase(Locale.ENGLISH)
if (name in unsafeHeaderNames || name.startsWith("proxy-")) return false
if (name == "connection" && value == "upgrade") return false
return true
}
private val unsafeHeaderNames = listOf(
"content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie",
)
@@ -11,7 +11,9 @@ import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
import kotlinx.coroutines.suspendCancellableCoroutine
import logcat.LogPriority
import okhttp3.Headers
import tachiyomi.core.common.util.system.logcat
import java.util.Locale
import kotlin.coroutines.resume
object WebViewUtil {
@@ -144,6 +146,31 @@ fun WebView.setUserAgent(userAgent: String) {
}
}
fun Headers.toWebViewHeaders(): Map<String, String> {
return this
// Keeping unsafe header makes webview throw [net::ERR_INVALID_ARGUMENT]
.filter { (name, value) ->
isRequestHeaderSafe(name, value)
}
.groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value }
.mapValues { it.value.getOrNull(0).orEmpty() }
}
// Based on [IsRequestHeaderSafe] in
// https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc
private fun isRequestHeaderSafe(_name: String, _value: String): Boolean {
val name = _name.lowercase(Locale.ENGLISH)
val value = _value.lowercase(Locale.ENGLISH)
if (name in unsafeHeaderNames || name.startsWith("proxy-")) return false
if (name == "connection" && value == "upgrade") return false
return true
}
private val unsafeHeaderNames = listOf(
"content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie",
// Renderer is killed if it doesn't match the request initiator
"origin",
)
private const val WEBVIEW_BRAND = "Android WebView"
private const val CHROMIUM_BRAND = "Chromium"
private const val CHROME_BRAND = "Google Chrome"