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

(cherry picked from commit 793d7fbe40c87ed233da8cc99d544d01024ed4f5)
This commit is contained in:
AwkwardPeak7 2025-03-02 20:16:42 +05:00 committed by Cuong-Tran
parent 2167ceba04
commit 6bfb80fe37
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
5 changed files with 89 additions and 20 deletions

View file

@ -34,6 +34,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 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 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 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 ### Removed
- Remove alphabetical category sort option - Remove alphabetical category sort option

View file

@ -3,6 +3,7 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.kevinnzou.web.AccompanistWebViewClient 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.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig 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.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Request
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun WebViewScreenContent( fun WebViewScreenContent(
@ -58,8 +65,11 @@ fun WebViewScreenContent(
) { ) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers) val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator() val navigator = rememberWebViewNavigator()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val network = remember { Injekt.get<NetworkHelper>() }
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
var currentUrl by remember { mutableStateOf(url) } var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) } var showCloudflareHelp by remember { mutableStateOf(false) }
@ -114,6 +124,40 @@ fun WebViewScreenContent(
} }
return super.shouldOverrideUrlLoading(view, request) 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

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

View file

@ -30,19 +30,16 @@ import kotlin.random.Random
/* SY --> */ open /* SY <-- */val cookieJar = AndroidCookieJar() /* SY --> */ open /* SY <-- */val cookieJar = AndroidCookieJar()
/* SY --> */ open /* SY <-- */val client: OkHttpClient =
// KMK -->
clientWithTimeOut()
/** /**
* Timeout in unit of seconds. * Timeout in unit of seconds.
*/ */
fun clientWithTimeOut( private /* KMK --> */ fun /* KMK <-- */ clientBuilder(
// KMK -->
connectTimeout: Long = 30, connectTimeout: Long = 30,
readTimeout: Long = 30, readTimeout: Long = 30,
callTimeout: Long = 120, callTimeout: Long = 120,
// KMK <-- // KMK <--
): OkHttpClient = run { ): OkHttpClient.Builder = run {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.cookieJar(cookieJar) .cookieJar(cookieJar)
// KMK --> // KMK -->
@ -68,10 +65,6 @@ import kotlin.random.Random
builder.addNetworkInterceptor(httpLoggingInterceptor) builder.addNetworkInterceptor(httpLoggingInterceptor)
} }
builder.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
when (preferences.dohProvider().get()) { when (preferences.dohProvider().get()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle() PREF_DOH_GOOGLE -> builder.dohGoogle()
@ -85,12 +78,34 @@ import kotlin.random.Random
PREF_DOH_CONTROLD -> builder.dohControlD() PREF_DOH_CONTROLD -> builder.dohControlD()
PREF_DOH_NJALLA -> builder.dohNajalla() PREF_DOH_NJALLA -> builder.dohNajalla()
PREF_DOH_SHECAN -> builder.dohShecan() PREF_DOH_SHECAN -> builder.dohShecan()
else -> builder
}
} }
builder.build() val nonCloudflareClient /* KMK --> */ by lazy /* KMK <-- */ { clientBuilder().build() }
/* SY --> */ open /* SY <-- */ val client /* KMK --> */ by lazy /* KMK <-- */ {
clientBuilder()
.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
.build()
} }
// KMK --> // KMK -->
/**
* Timeout in unit of seconds.
*/
private fun clientWithTimeOut(
connectTimeout: Long = 30,
readTimeout: Long = 30,
callTimeout: Long = 120,
) = clientBuilder(connectTimeout, readTimeout, callTimeout)
.addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
)
.build()
/** /**
* Allow to download a big file with retry & resume capability because * Allow to download a big file with retry & resume capability because
* normally it would get a Timeout exception. * normally it would get a Timeout exception.

View file

@ -12,7 +12,8 @@ import tachiyomi.core.common.util.system.logcat
import kotlin.coroutines.resume import kotlin.coroutines.resume
object WebViewUtil { 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 const val MINIMUM_WEBVIEW_VERSION = 118
@ -53,6 +54,16 @@ object WebViewUtil {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW) 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 { fun WebView.isOutdated(): Boolean {