Spoof or remove X-Requested-With header from webview (#1812)

This commit is contained in:
AwkwardPeak7 2025-03-02 20:16:42 +05:00 committed by GitHub
parent b12ee027ea
commit 793d7fbe40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 16 deletions

View File

@ -35,6 +35,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Fix certain Infinix, Xiaomi devices being unable to use any "Open link in browser" actions, including tracker setup ([@MajorTanya](https://github.com/MajorTanya)) ([#1684](https://github.com/mihonapp/mihon/pull/1684)) ([#1776](https://github.com/mihonapp/mihon/pull/1776))
- Fix App's preferences referencing deleted categories ([@cuong-tran](https://github.com/cuong-tran)) ([#1734](https://github.com/mihonapp/mihon/pull/1734))
- Fix backup/restore of category related preferences ([@cuong-tran](https://github.com/cuong-tran)) ([#1726](https://github.com/mihonapp/mihon/pull/1726))
- Fix WebView sending app's package name in `X-Requested-With` header, which led to sources blocking access ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1812](https://github.com/mihonapp/mihon/pull/1812))
### Removed
- Remove alphabetical category sort option

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import com.kevinnzou.web.AccompanistWebViewClient
@ -37,13 +39,18 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import okhttp3.Request
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun WebViewScreenContent(
@ -58,8 +65,11 @@ fun WebViewScreenContent(
) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
val network = remember { Injekt.get<NetworkHelper>() }
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) }
@ -114,6 +124,40 @@ fun WebViewScreenContent(
}
return super.shouldOverrideUrlLoading(view, request)
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
return try {
val internalRequest = Request.Builder().apply {
url(request!!.url.toString())
request.requestHeaders.forEach { (key, value) ->
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
return@forEach
}
addHeader(key, value)
}
method(request.method, null)
}.build()
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
WebResourceResponse(
contentType,
contentEncoding,
response.code,
response.message,
response.headers.associate { it.first to it.second },
response.body.byteStream(),
)
} catch (e: Throwable) {
super.shouldInterceptRequest(view, request)
}
}
}
}

View File

@ -219,17 +219,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
try {
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val chromiumElement = stackTrace.find {
it.className.equals(
"org.chromium.base.BuildInfo",
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
val isChromiumCall = stackTrace.any {
it.className.startsWith("org.chromium.") &&
it.methodName in setOf("getAll", "getPackageName", "<init>")
}
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
} catch (_: Exception) {
}
return super.getPackageName()
}

View File

@ -19,7 +19,7 @@ class NetworkHelper(
val cookieJar = AndroidCookieJar()
val client: OkHttpClient = run {
private val clientBuilder: OkHttpClient.Builder = run {
val builder = OkHttpClient.Builder()
.cookieJar(cookieJar)
.connectTimeout(30, TimeUnit.SECONDS)
@ -43,10 +43,6 @@ class NetworkHelper(
builder.addNetworkInterceptor(httpLoggingInterceptor)
}
builder.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
when (preferences.dohProvider().get()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
@ -60,11 +56,18 @@ class NetworkHelper(
PREF_DOH_CONTROLD -> builder.dohControlD()
PREF_DOH_NJALLA -> builder.dohNajalla()
PREF_DOH_SHECAN -> builder.dohShecan()
else -> builder
}
builder.build()
}
val nonCloudflareClient = clientBuilder.build()
val client = clientBuilder
.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
.build()
/**
* @deprecated Since extension-lib 1.5
*/

View File

@ -12,7 +12,8 @@ import tachiyomi.core.common.util.system.logcat
import kotlin.coroutines.resume
object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
private const val CHROME_PACKAGE = "com.android.chrome"
private const val SYSTEM_SETTINGS_PACKAGE = "com.android.settings"
const val MINIMUM_WEBVIEW_VERSION = 118
@ -53,6 +54,16 @@ object WebViewUtil {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW)
}
fun spoofedPackageName(context: Context): String {
return try {
context.packageManager.getPackageInfo(CHROME_PACKAGE, PackageManager.GET_META_DATA)
CHROME_PACKAGE
} catch (_: PackageManager.NameNotFoundException) {
SYSTEM_SETTINGS_PACKAGE
}
}
}
fun WebView.isOutdated(): Boolean {