mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Spoof or remove X-Requested-With header from webview (#1812)
				
					
				
			This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|      */ | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user