From 8a0e49232eb499ae0108be98d660cbb66c574c43 Mon Sep 17 00:00:00 2001 From: Stefan <84-stefan@users.noreply.gitlab.futo.org> Date: Fri, 27 Feb 2026 06:59:35 +0000 Subject: [PATCH] feat: propagate WebView user agent to plugins via bridge.captchaUserAgent and bridge.authUserAgent --- .../platformplayer/activities/CaptchaActivity.kt | 8 ++++++-- .../platformplayer/activities/LoginActivity.kt | 8 ++++++-- .../api/media/platforms/js/JSClient.kt | 3 +++ .../api/media/platforms/js/SourceAuth.kt | 14 ++++++++------ .../api/media/platforms/js/SourceCaptchaData.kt | 14 ++++++++------ .../com/futo/platformplayer/engine/V8Plugin.kt | 4 +++- .../engine/packages/PackageBridge.kt | 15 +++++++++++++++ .../fragment/mainactivity/main/LoginFragment.kt | 8 ++++++-- .../platformplayer/others/CaptchaWebViewClient.kt | 10 +++++++--- .../platformplayer/others/LoginWebViewClient.kt | 10 +++++++--- 10 files changed, 69 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/activities/CaptchaActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/CaptchaActivity.kt index a5706494..060b61ab 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/CaptchaActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/CaptchaActivity.kt @@ -68,11 +68,15 @@ class CaptchaActivity : AppCompatActivity() { intent.getStringExtra("body"); else null; - _webView.settings.userAgentString = captchaConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"; + // Only override UA if config specifies one, otherwise keep WebView default (matches Desktop CEF behavior) + if (captchaConfig.userAgent != null) + _webView.settings.userAgentString = captchaConfig.userAgent; + // Capture UA on main thread - callback fires on WebView background thread where settings access is not allowed + val capturedUserAgent = _webView.settings.userAgentString; _webView.settings.useWideViewPort = true; _webView.settings.loadWithOverviewMode = true; - val webViewClient = if(config != null) CaptchaWebViewClient(config) else CaptchaWebViewClient(captchaConfig); + val webViewClient = if(config != null) CaptchaWebViewClient(config, capturedUserAgent) else CaptchaWebViewClient(captchaConfig, capturedUserAgent); webViewClient.onCaptchaFinished.subscribe { captcha -> _callback?.let { _callback = null; diff --git a/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt index 80062a24..905f192d 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt @@ -61,11 +61,15 @@ class LoginActivity : AppCompatActivity() { else throw IllegalStateException("No valid configuration?"); //TODO: Backwards compat removal? - _webView.settings.userAgentString = authConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"; + // Only override UA if config specifies one, otherwise keep WebView default (matches Desktop CEF behavior) + if (authConfig.userAgent != null) + _webView.settings.userAgentString = authConfig.userAgent; + // Capture UA on main thread - callback fires on WebView background thread where settings access is not allowed + val capturedUserAgent = _webView.settings.userAgentString; _webView.settings.useWideViewPort = true; _webView.settings.loadWithOverviewMode = true; - val webViewClient = if(config != null) LoginWebViewClient(config) else LoginWebViewClient(authConfig); + val webViewClient = if(config != null) LoginWebViewClient(config, capturedUserAgent) else LoginWebViewClient(authConfig, capturedUserAgent); webViewClient.onLogin.subscribe { auth -> _callback?.let { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 1718acb3..f2c53733 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -55,6 +55,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptValidationException +import com.futo.platformplayer.engine.packages.PackageBridge import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.states.AnnouncementType @@ -156,6 +157,7 @@ open class JSClient : IPlatformClient { _httpClient = JSHttpClient(this, null, _captcha, config); _httpClientAuth = JSHttpClient(this, _auth, _captcha, config); _plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth); + _plugin.bridge.descriptor = descriptor; _plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/source.js"); @@ -189,6 +191,7 @@ open class JSClient : IPlatformClient { _httpClient = JSHttpClient(this, null, _captcha, config); _httpClientAuth = JSHttpClient(this, _auth, _captcha, config); _plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth); + _plugin.bridge.descriptor = descriptor; _plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/source.js"); _plugin.withScript(script); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt index d2c705b8..e511c162 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt @@ -5,9 +5,9 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -data class SourceAuth(val cookieMap: HashMap>? = null, val headers: Map> = mapOf()) { +data class SourceAuth(val cookieMap: HashMap>? = null, val headers: Map> = mapOf(), val userAgent: String? = null) { override fun toString(): String { - return "(headers: '$headers', cookieString: '$cookieMap')"; + return "(headers: '$headers', cookieString: '$cookieMap', userAgent: '$userAgent')"; } fun toEncrypted(): String{ @@ -15,23 +15,25 @@ data class SourceAuth(val cookieMap: HashMap>? = } private fun serialize(): String { - return Json.encodeToString(SerializedAuth(cookieMap, headers)); + return Json.encodeToString(SerializedAuth(cookieMap, headers, userAgent)); } companion object { val TAG = "SourceAuth"; + private val _json = Json { ignoreUnknownKeys = true }; fun fromEncrypted(encrypted: String?): SourceAuth? { return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) }; } private fun deserialize(str: String): SourceAuth { - val data = Json.decodeFromString(str); - return SourceAuth(data.cookieMap, data.headers); + val data = _json.decodeFromString(str); + return SourceAuth(data.cookieMap, data.headers, data.userAgent); } } @Serializable data class SerializedAuth(val cookieMap: HashMap>?, - val headers: Map> = mapOf()) + val headers: Map> = mapOf(), + val userAgent: String? = null) } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt index 140a19d0..51e09715 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt @@ -5,9 +5,9 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -data class SourceCaptchaData(val cookieMap: HashMap>? = null, val headers: Map> = mapOf()) { +data class SourceCaptchaData(val cookieMap: HashMap>? = null, val headers: Map> = mapOf(), val userAgent: String? = null) { override fun toString(): String { - return "(headers: '$headers', cookieString: '$cookieMap')"; + return "(headers: '$headers', cookieString: '$cookieMap', userAgent: '$userAgent')"; } fun toEncrypted(): String{ @@ -15,23 +15,25 @@ data class SourceCaptchaData(val cookieMap: HashMap(str); - return SourceCaptchaData(data.cookieMap, data.headers); + val data = _json.decodeFromString(str); + return SourceCaptchaData(data.cookieMap, data.headers, data.userAgent); } } @Serializable data class SerializedCaptchaData(val cookieMap: HashMap>?, - val headers: Map> = mapOf()) + val headers: Map> = mapOf(), + val userAgent: String? = null) } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 1c44aed1..ffe9e99a 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -87,6 +87,7 @@ class V8Plugin { private val _deps : LinkedHashMap = LinkedHashMap(); private val _depsPackages : MutableList = mutableListOf(); private var _script : String? = null; + val bridge: PackageBridge; var isStopped = true; val onStopped = Event1(); @@ -114,7 +115,8 @@ class V8Plugin { this._clientAuth = clientAuth; this.config = config; this._script = script; - withDependency(PackageBridge(this, config)); + bridge = PackageBridge(this, config); + withDependency(bridge); for(pack in config.packages) withDependency(getPackage(pack)!!); diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index 6c7dab49..ca2399f3 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt @@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClientConstants import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin @@ -36,6 +37,9 @@ class PackageBridge : V8Package { private val _client: ManagedHttpClient @Transient private val _clientAuth: ManagedHttpClient + // Set by JSClient after construction to provide access to auth/captcha data + @Transient + var descriptor: SourcePluginDescriptor? = null override val name: String get() = "Bridge"; @@ -80,6 +84,17 @@ class PackageBridge : V8Package { return "android"; } + // User agent captured during captcha/auth WebView flows, matching Desktop's bridge.captchaUserAgent/bridge.authUserAgent. + // Plugins use these to make HTTP requests with the same UA that was used in the WebView. + @V8Property + fun captchaUserAgent(): String? { + return descriptor?.getCaptchaData()?.userAgent + } + @V8Property + fun authUserAgent(): String? { + return descriptor?.getAuth()?.userAgent + } + @V8Property fun supportedFeatures(): Array { return arrayOf( diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt index 9b05816c..b1f84864 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt @@ -96,11 +96,15 @@ class LoginFragment : MainFragment() { else throw IllegalStateException("No valid configuration?"); //TODO: Backwards compat removal? - _webView.settings.userAgentString = authConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"; + // Only override UA if config specifies one, otherwise keep WebView default (matches Desktop CEF behavior) + if (authConfig.userAgent != null) + _webView.settings.userAgentString = authConfig.userAgent; + // Capture UA on main thread - callback fires on WebView background thread where settings access is not allowed + val capturedUserAgent = _webView.settings.userAgentString; _webView.settings.useWideViewPort = true; _webView.settings.loadWithOverviewMode = true; - val webViewClient = if(config != null) LoginWebViewClient(config) else LoginWebViewClient(authConfig); + val webViewClient = if(config != null) LoginWebViewClient(config, capturedUserAgent) else LoginWebViewClient(authConfig, capturedUserAgent); webViewClient.onLogin.subscribe { auth -> _callback?.let { diff --git a/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt b/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt index 90ae414a..7adc8920 100644 --- a/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt +++ b/app/src/main/java/com/futo/platformplayer/others/CaptchaWebViewClient.kt @@ -16,13 +16,15 @@ class CaptchaWebViewClient : WebViewClient { private val _pluginConfig: SourcePluginConfig?; private val _captchaConfig: SourcePluginCaptchaConfig; + private val _userAgent: String?; private var _didNotify = false; private val _extractor: WebViewRequirementExtractor; - constructor(config: SourcePluginConfig) : super() { + constructor(config: SourcePluginConfig, userAgent: String? = null) : super() { _pluginConfig = config; _captchaConfig = config.captcha!!; + _userAgent = userAgent; _extractor = WebViewRequirementExtractor( config.allowUrls, null, @@ -34,9 +36,10 @@ class CaptchaWebViewClient : WebViewClient { Logger.i(TAG, "Captcha [${config.name}]" + "\nRequired Cookies: ${Serializer.json.encodeToString(config.captcha!!.cookiesToFind)}",); } - constructor(captcha: SourcePluginCaptchaConfig) : super() { + constructor(captcha: SourcePluginCaptchaConfig, userAgent: String? = null) : super() { _pluginConfig = null; _captchaConfig = captcha; + _userAgent = userAgent; _extractor = WebViewRequirementExtractor( null, null, @@ -62,7 +65,8 @@ class CaptchaWebViewClient : WebViewClient { _didNotify = true; onCaptchaFinished.emit(SourceCaptchaData( extracted.cookies, - extracted.headers + extracted.headers, + _userAgent )); } diff --git a/app/src/main/java/com/futo/platformplayer/others/LoginWebViewClient.kt b/app/src/main/java/com/futo/platformplayer/others/LoginWebViewClient.kt index 1b31a5fc..7dcc1349 100644 --- a/app/src/main/java/com/futo/platformplayer/others/LoginWebViewClient.kt +++ b/app/src/main/java/com/futo/platformplayer/others/LoginWebViewClient.kt @@ -24,23 +24,26 @@ class LoginWebViewClient : WebViewClient { private val _pluginConfig: SourcePluginConfig?; private val _authConfig: SourcePluginAuthConfig; + private val _userAgent: String?; private val _client = ManagedHttpClient(); val onLogin = Event1(); val onPageLoaded = Event2() - constructor(config: SourcePluginConfig) : super() { + constructor(config: SourcePluginConfig, userAgent: String? = null) : super() { _pluginConfig = config; _authConfig = config.authentication!!; + _userAgent = userAgent; Logger.i(TAG, "Login [${config.name}]" + "\nRequired Headers: ${config.authentication.headersToFind?.joinToString(", ")}" + "\nRequired Domain Headers: ${Serializer.json.encodeToString(config.authentication.domainHeadersToFind)}" + "\nRequired Cookies: ${Serializer.json.encodeToString(config.authentication.cookiesToFind)}",); } - constructor(auth: SourcePluginAuthConfig) : super() { + constructor(auth: SourcePluginAuthConfig, userAgent: String? = null) : super() { _pluginConfig = null; _authConfig = auth; + _userAgent = userAgent; } private val headersFoundMap: HashMap> = hashMapOf(); @@ -192,13 +195,14 @@ class LoginWebViewClient : WebViewClient { if (urlFound && headersFound && domainHeadersFound && cookiesFound) { onLogin.emit(SourceAuth( cookieMap = cookiesFoundMap, - headers = headersFoundMap /*.associate { headerToFind -> + headers = headersFoundMap, /*.associate { headerToFind -> headerToFind to headersFoundMap.firstNotNullOf { requestHeader -> if (requestHeader.key.equals(headerToFind, ignoreCase = true)) requestHeader.value else null; } } ?: mapOf()*/ + userAgent = _userAgent )); }