Improve WebView spoofing (#3678)
- Spoof `Sec-CH-UA` client hints to match the user agent - Fix `X-Requested-With` spoofing leaking to unrelated callers - Update default user agent to Chrome 149
This commit is contained in:
@@ -14,6 +14,6 @@ class NetworkPreferences(
|
||||
|
||||
val defaultUserAgent: Preference<String> = preferenceStore.getString(
|
||||
"default_user_agent",
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Mobile Safari/537.36",
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.setUserAgent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
@@ -84,7 +85,7 @@ abstract class WebViewInterceptor(
|
||||
return WebView(context).apply {
|
||||
setDefaultSettings()
|
||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider()
|
||||
setUserAgent(request.header("User-Agent") ?: defaultUserAgentProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.content.pm.PackageManager
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import androidx.webkit.UserAgentMetadata
|
||||
import androidx.webkit.WebSettingsCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -99,6 +102,53 @@ fun WebView.setDefaultSettings() {
|
||||
CookieManager.getInstance().acceptThirdPartyCookies(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user agent along with the matching user agent metadata, which Chromium uses to populate
|
||||
* the `Sec-CH-UA` client hints. Without this the hints keep advertising the real WebView brand and
|
||||
* version, contradicting the spoofed user agent.
|
||||
*/
|
||||
fun WebView.setUserAgent(userAgent: String) {
|
||||
settings.userAgentString = userAgent
|
||||
|
||||
if (!WebViewFeature.isFeatureSupported(WebViewFeature.USER_AGENT_METADATA)) return
|
||||
|
||||
val versionMatch = CHROME_VERSION_REGEX.find(userAgent) ?: return
|
||||
val majorVersion = versionMatch.groupValues[1]
|
||||
val fullVersion = majorVersion + versionMatch.groupValues[2].ifEmpty { ".0.0.0" }
|
||||
|
||||
try {
|
||||
val metadata = WebSettingsCompat.getUserAgentMetadata(settings)
|
||||
val brandVersionList = metadata.brandVersionList.map { brandVersion ->
|
||||
val brand = when (brandVersion.brand) {
|
||||
WEBVIEW_BRAND -> CHROME_BRAND
|
||||
CHROMIUM_BRAND -> CHROMIUM_BRAND
|
||||
else -> return@map brandVersion
|
||||
}
|
||||
|
||||
UserAgentMetadata.BrandVersion.Builder()
|
||||
.setBrand(brand)
|
||||
.setMajorVersion(majorVersion)
|
||||
.setFullVersion(fullVersion)
|
||||
.build()
|
||||
}
|
||||
|
||||
WebSettingsCompat.setUserAgentMetadata(
|
||||
settings,
|
||||
UserAgentMetadata.Builder(metadata)
|
||||
.setBrandVersionList(brandVersionList)
|
||||
.setFullVersion(fullVersion)
|
||||
.build(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to set user agent metadata" }
|
||||
}
|
||||
}
|
||||
|
||||
private const val WEBVIEW_BRAND = "Android WebView"
|
||||
private const val CHROMIUM_BRAND = "Chromium"
|
||||
private const val CHROME_BRAND = "Google Chrome"
|
||||
private val CHROME_VERSION_REGEX = """Chrome/(\d+)(\.[\d.]+)?""".toRegex()
|
||||
|
||||
private fun WebView.getWebViewMajorVersion(): Int {
|
||||
val uaRegexMatch = """.*Chrome/(\d+)\..*""".toRegex().matchEntire(getDefaultUserAgentString())
|
||||
return if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) {
|
||||
|
||||
Reference in New Issue
Block a user